Procházet zdrojové kódy

Merge pull request #107391 from BastiaanOlij/openxr_spatial_entities_ext

OpenXR: Add support for spatial entities extension
Thaddeus Crews před 2 týdny
rodič
revize
8d8041bd4d
54 změnil soubory, kde provedl 7550 přidání a 17 odebrání
  1. 38 0
      doc/classes/ProjectSettings.xml
  2. 10 0
      main/main.cpp
  3. 30 0
      modules/openxr/config.py
  4. 1 1
      modules/openxr/doc_classes/OpenXRAPIExtension.xml
  5. 31 0
      modules/openxr/doc_classes/OpenXRAnchorTracker.xml
  6. 38 0
      modules/openxr/doc_classes/OpenXRMarkerTracker.xml
  7. 65 0
      modules/openxr/doc_classes/OpenXRPlaneTracker.xml
  8. 100 0
      modules/openxr/doc_classes/OpenXRSpatialAnchorCapability.xml
  9. 20 0
      modules/openxr/doc_classes/OpenXRSpatialCapabilityConfigurationAnchor.xml
  10. 40 0
      modules/openxr/doc_classes/OpenXRSpatialCapabilityConfigurationAprilTag.xml
  11. 76 0
      modules/openxr/doc_classes/OpenXRSpatialCapabilityConfigurationAruco.xml
  12. 31 0
      modules/openxr/doc_classes/OpenXRSpatialCapabilityConfigurationBaseHeader.xml
  13. 20 0
      modules/openxr/doc_classes/OpenXRSpatialCapabilityConfigurationMicroQrCode.xml
  14. 38 0
      modules/openxr/doc_classes/OpenXRSpatialCapabilityConfigurationPlaneTracking.xml
  15. 20 0
      modules/openxr/doc_classes/OpenXRSpatialCapabilityConfigurationQrCode.xml
  16. 20 0
      modules/openxr/doc_classes/OpenXRSpatialComponentAnchorList.xml
  17. 27 0
      modules/openxr/doc_classes/OpenXRSpatialComponentBounded2DList.xml
  18. 27 0
      modules/openxr/doc_classes/OpenXRSpatialComponentBounded3DList.xml
  19. 40 0
      modules/openxr/doc_classes/OpenXRSpatialComponentData.xml
  20. 55 0
      modules/openxr/doc_classes/OpenXRSpatialComponentMarkerList.xml
  21. 36 0
      modules/openxr/doc_classes/OpenXRSpatialComponentMesh2DList.xml
  22. 27 0
      modules/openxr/doc_classes/OpenXRSpatialComponentMesh3DList.xml
  23. 20 0
      modules/openxr/doc_classes/OpenXRSpatialComponentParentList.xml
  24. 27 0
      modules/openxr/doc_classes/OpenXRSpatialComponentPersistenceList.xml
  25. 34 0
      modules/openxr/doc_classes/OpenXRSpatialComponentPlaneAlignmentList.xml
  26. 37 0
      modules/openxr/doc_classes/OpenXRSpatialComponentPlaneSemanticLabelList.xml
  27. 28 0
      modules/openxr/doc_classes/OpenXRSpatialComponentPolygon2DList.xml
  28. 27 0
      modules/openxr/doc_classes/OpenXRSpatialContextPersistenceConfig.xml
  29. 277 0
      modules/openxr/doc_classes/OpenXRSpatialEntityExtension.xml
  30. 38 0
      modules/openxr/doc_classes/OpenXRSpatialEntityTracker.xml
  31. 37 0
      modules/openxr/doc_classes/OpenXRSpatialMarkerTrackingCapability.xml
  32. 19 0
      modules/openxr/doc_classes/OpenXRSpatialPlaneTrackingCapability.xml
  33. 33 0
      modules/openxr/doc_classes/OpenXRSpatialQueryResultData.xml
  34. 30 0
      modules/openxr/doc_classes/OpenXRStructureBase.xml
  35. 1 0
      modules/openxr/extensions/SCsub
  36. 1 1
      modules/openxr/extensions/openxr_extension_wrapper.h
  37. 1 1
      modules/openxr/extensions/openxr_future_extension.h
  38. 1184 0
      modules/openxr/extensions/spatial_entities/openxr_spatial_anchor.cpp
  39. 256 0
      modules/openxr/extensions/spatial_entities/openxr_spatial_anchor.h
  40. 504 0
      modules/openxr/extensions/spatial_entities/openxr_spatial_entities.cpp
  41. 230 0
      modules/openxr/extensions/spatial_entities/openxr_spatial_entities.h
  42. 1215 0
      modules/openxr/extensions/spatial_entities/openxr_spatial_entity_extension.cpp
  43. 217 0
      modules/openxr/extensions/spatial_entities/openxr_spatial_entity_extension.h
  44. 788 0
      modules/openxr/extensions/spatial_entities/openxr_spatial_marker_tracking.cpp
  45. 268 0
      modules/openxr/extensions/spatial_entities/openxr_spatial_marker_tracking.h
  46. 879 0
      modules/openxr/extensions/spatial_entities/openxr_spatial_plane_tracking.cpp
  47. 254 0
      modules/openxr/extensions/spatial_entities/openxr_spatial_plane_tracking.h
  48. 25 6
      modules/openxr/openxr_api.cpp
  49. 1 0
      modules/openxr/openxr_api.h
  50. 83 0
      modules/openxr/openxr_structure.cpp
  51. 76 0
      modules/openxr/openxr_structure.h
  52. 102 8
      modules/openxr/openxr_util.cpp
  53. 13 0
      modules/openxr/openxr_util.h
  54. 55 0
      modules/openxr/register_types.cpp

+ 38 - 0
doc/classes/ProjectSettings.xml

@@ -3466,6 +3466,44 @@
 			If [code]true[/code] we enable the render model extension if available.
 			[b]Note:[/b] This relates to the core OpenXR render model extension and has no relation to any vendor render model extensions.
 		</member>
+		<member name="xr/openxr/extensions/spatial_entity/april_tag_dict" type="int" setter="" getter="" default="&quot;3&quot;">
+			The April Tag marker types the built-in marker tracking is set to recognize (if April Tag marker tracking is available and enabled).
+		</member>
+		<member name="xr/openxr/extensions/spatial_entity/aruco_dict" type="int" setter="" getter="" default="&quot;15&quot;">
+			The ArUco marker types the built-in marker tracking is set to recognize (if ArUco marker tracking is available and enabled).
+		</member>
+		<member name="xr/openxr/extensions/spatial_entity/enable_builtin_anchor_detection" type="bool" setter="" getter="" default="false">
+			If [code]true[/code], we enable the built-in logic for handling anchors. Godot will query (persistent) anchors and manage [OpenXRAnchorTracker] instances for you. If disabled you'll need to create your own spatial and persistence context and perform your own discovery queries.
+			[b]Note:[/b] This functionality requires that spatial anchors are supported and enabled.
+		</member>
+		<member name="xr/openxr/extensions/spatial_entity/enable_builtin_marker_tracking" type="bool" setter="" getter="" default="false">
+			If [code]true[/code], we enable the built-in logic for handling marker tracking. Godot will query markers and manage [OpenXRMarkerTracker] instances for you. If disabled you'll need to create your own spatial context and perform your own discovery queries.
+			[b]Note:[/b] This functionality requires that marker tracking is supported and enabled.
+		</member>
+		<member name="xr/openxr/extensions/spatial_entity/enable_builtin_plane_detection" type="bool" setter="" getter="" default="false">
+			If [code]true[/code], we enable the built-in logic for handling plane detection. Godot will query detected planes (walls, floors, ceilings, etc.) and manage [OpenXRPlaneTracker] instances for you. If disabled you'll need to create your own spatial context and perform your own discovery queries.
+			[b]Note:[/b] This functionality requires that plane tracking is supported and enabled.
+		</member>
+		<member name="xr/openxr/extensions/spatial_entity/enable_marker_tracking" type="bool" setter="" getter="" default="false">
+			If [code]true[/code], support for the marker tracking extension is requested. If supported, you will be able to query information about markers detected by the XR runtime, e.g. QR codes, aruca markers and april tags.
+			[b]Note:[/b] This requires that the OpenXR spatial entities and marker tracking extensions are supported by the XR runtime. If not supported this setting will be ignored. [member xr/openxr/extensions/spatial_entity/enabled] must be enabled for this setting to be used.
+		</member>
+		<member name="xr/openxr/extensions/spatial_entity/enable_persistent_anchors" type="bool" setter="" getter="" default="false">
+			If [code]true[/code], support for the persistent anchors extension is requested. If supported, you will be able to store spatial anchors and they will be restored on application startup.
+			[b]Note:[/b] This requires that the OpenXR spatial entities, spatial anchors, and spatial persistence extensions are supported by the XR runtime. If not supported this setting will be ignored. [member xr/openxr/extensions/spatial_entity/enabled] and [member xr/openxr/extensions/spatial_entity/enable_spatial_anchors] must be enabled for this setting to be used.
+		</member>
+		<member name="xr/openxr/extensions/spatial_entity/enable_plane_tracking" type="bool" setter="" getter="" default="false">
+			If [code]true[/code], support for the plane tracking extension is requested. If supported, you will be able to query information about planes detected by the XR runtime, e.g. walls, floors, etc.
+			[b]Note:[/b] This requires that the OpenXR spatial entities and plane tracking extensions are supported by the XR runtime. If not supported this setting will be ignored. [member xr/openxr/extensions/spatial_entity/enabled] must be enabled for this setting to be used.
+		</member>
+		<member name="xr/openxr/extensions/spatial_entity/enable_spatial_anchors" type="bool" setter="" getter="" default="false">
+			If [code]true[/code], support for the spatial anchors extension is requested. If supported, you will be able to register anchor locations in the real world that the XR runtime will adjust as needed and/or potentially share with other headsets.
+			[b]Note:[/b] This requires that the OpenXR spatial entities and spatial anchors extensions are supported by the XR runtime. If not supported this setting will be ignored. [member xr/openxr/extensions/spatial_entity/enabled] must be enabled for this setting to be used.
+		</member>
+		<member name="xr/openxr/extensions/spatial_entity/enabled" type="bool" setter="" getter="" default="false">
+			If [code]true[/code], support for the spatial entity extension is requested. If supported, you will be able to access spatial information about the real environment around you. What information is available is dependent on additional extensions.
+			[b]Note:[/b] This requires that the OpenXR spatial entities extension is supported by the XR runtime. If not supported this setting will be ignored.
+		</member>
 		<member name="xr/openxr/form_factor" type="int" setter="" getter="" default="&quot;0&quot;">
 			Specify whether OpenXR should be configured for an HMD or a hand held device.
 		</member>

+ 10 - 0
main/main.cpp

@@ -2805,6 +2805,16 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
 	GLOBAL_DEF_BASIC("xr/openxr/extensions/hand_tracking_unobstructed_data_source", false); // XR_HAND_TRACKING_DATA_SOURCE_UNOBSTRUCTED_EXT
 	GLOBAL_DEF_BASIC("xr/openxr/extensions/hand_tracking_controller_data_source", false); // XR_HAND_TRACKING_DATA_SOURCE_CONTROLLER_EXT
 	GLOBAL_DEF_RST_BASIC("xr/openxr/extensions/hand_interaction_profile", false);
+	GLOBAL_DEF_BASIC("xr/openxr/extensions/spatial_entity/enabled", false);
+	GLOBAL_DEF_BASIC("xr/openxr/extensions/spatial_entity/enable_spatial_anchors", false);
+	GLOBAL_DEF_BASIC("xr/openxr/extensions/spatial_entity/enable_persistent_anchors", false);
+	GLOBAL_DEF_BASIC("xr/openxr/extensions/spatial_entity/enable_builtin_anchor_detection", false);
+	GLOBAL_DEF_BASIC("xr/openxr/extensions/spatial_entity/enable_plane_tracking", false);
+	GLOBAL_DEF_BASIC("xr/openxr/extensions/spatial_entity/enable_builtin_plane_detection", false);
+	GLOBAL_DEF_BASIC("xr/openxr/extensions/spatial_entity/enable_marker_tracking", false);
+	GLOBAL_DEF_BASIC("xr/openxr/extensions/spatial_entity/enable_builtin_marker_tracking", false);
+	GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "xr/openxr/extensions/spatial_entity/aruco_dict", PROPERTY_HINT_ENUM, "4x4 50 IDs,4x4 100 IDs,4x4 250 IDs,4x4 1000 IDs,5x5 50 IDs,5x5 100 IDs,5x5 250 IDs,5x5 1000 IDs,6x6 50 IDs,6x6 100 IDs,6x6 250 IDs,6x6 1000 IDs,7x7 50 IDs,7x7 100 IDs,7x7 250 IDs,7x7 1000 IDs"), "15");
+	GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "xr/openxr/extensions/spatial_entity/april_tag_dict", PROPERTY_HINT_ENUM, "4x4H5,5x5H9,6x6H10,6x6H11"), "3");
 	GLOBAL_DEF_RST_BASIC("xr/openxr/extensions/eye_gaze_interaction", false);
 	GLOBAL_DEF_BASIC("xr/openxr/extensions/render_model", false);
 

+ 30 - 0
modules/openxr/config.py

@@ -43,6 +43,36 @@ def get_doc_classes():
         "OpenXRRenderModelExtension",
         "OpenXRRenderModel",
         "OpenXRRenderModelManager",
+        "OpenXRStructureBase",
+        "OpenXRSpatialEntityExtension",
+        "OpenXRSpatialEntityTracker",
+        "OpenXRAnchorTracker",
+        "OpenXRPlaneTracker",
+        "OpenXRMarkerTracker",
+        "OpenXRSpatialCapabilityConfigurationBaseHeader",
+        "OpenXRSpatialCapabilityConfigurationAnchor",
+        "OpenXRSpatialCapabilityConfigurationQrCode",
+        "OpenXRSpatialCapabilityConfigurationMicroQrCode",
+        "OpenXRSpatialCapabilityConfigurationAruco",
+        "OpenXRSpatialCapabilityConfigurationAprilTag",
+        "OpenXRSpatialContextPersistenceConfig",
+        "OpenXRSpatialCapabilityConfigurationPlaneTracking",
+        "OpenXRSpatialComponentData",
+        "OpenXRSpatialComponentBounded2DList",
+        "OpenXRSpatialComponentBounded3DList",
+        "OpenXRSpatialComponentParentList",
+        "OpenXRSpatialComponentMesh2DList",
+        "OpenXRSpatialComponentMesh3DList",
+        "OpenXRSpatialComponentPlaneAlignmentList",
+        "OpenXRSpatialComponentPolygon2DList",
+        "OpenXRSpatialComponentPlaneSemanticLabelList",
+        "OpenXRSpatialComponentMarkerList",
+        "OpenXRSpatialQueryResultData",
+        "OpenXRSpatialComponentAnchorList",
+        "OpenXRSpatialComponentPersistenceList",
+        "OpenXRSpatialAnchorCapability",
+        "OpenXRSpatialPlaneTrackingCapability",
+        "OpenXRSpatialMarkerTrackingCapability",
     ]
 
 

+ 1 - 1
modules/openxr/doc_classes/OpenXRAPIExtension.xml

@@ -140,7 +140,7 @@
 		<method name="get_system_id">
 			<return type="int" />
 			<description>
-				Returns the id of the system, which is an [url=https://registry.khronos.org/OpenXR/specs/1.0/man/html/XrSystemId.html]XrSystemId[/url] cast to an integer.
+				Returns the ID of the system, which is an [url=https://registry.khronos.org/OpenXR/specs/1.0/man/html/XrSystemId.html]XrSystemId[/url] cast to an integer.
 			</description>
 		</method>
 		<method name="insert_debug_label">

+ 31 - 0
modules/openxr/doc_classes/OpenXRAnchorTracker.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRAnchorTracker" inherits="OpenXRSpatialEntityTracker" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Positional tracker for our spatial entity anchor extension.
+	</brief_description>
+	<description>
+		Positional tracker for our OpenXR spatial entity anchor extension, it tracks a user defined location in real space and maps it to our virtual space.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="has_uuid" qualifiers="const">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if a non-zero UUID is set.
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="uuid" type="String" setter="set_uuid" getter="get_uuid" default="&quot;&quot;">
+			The UUID provided for persistent anchors.
+		</member>
+	</members>
+	<signals>
+		<signal name="uuid_changed">
+			<description>
+				Emitted when the UUID for this anchor was changed.
+			</description>
+		</signal>
+	</signals>
+</class>

+ 38 - 0
modules/openxr/doc_classes/OpenXRMarkerTracker.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRMarkerTracker" inherits="OpenXRSpatialEntityTracker" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Spatial entity tracker for our spatial entity marker tracking extension.
+	</brief_description>
+	<description>
+		Spatial entity tracker for our OpenXR spatial entity marker tracking extension. These trackers identify entities in our real space detected by a visual marker such as a QRCode or Aruco code, and map their location to our virtual space.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_marker_data" qualifiers="const">
+			<return type="Variant" />
+			<description>
+				Returns the marker data for this marker. This can return a [String] or [PackedByteArray]. Only applicable to QR Code based markers.
+			</description>
+		</method>
+		<method name="set_marker_data">
+			<return type="void" />
+			<param index="0" name="marker_data" type="Variant" />
+			<description>
+				Sets the marker data for this marker.
+				[b]Note:[/b] This should only be set by marker discovery logic.
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="bounds_size" type="Vector2" setter="set_bounds_size" getter="get_bounds_size" default="Vector2(0, 0)">
+			The bounds size for this marker.
+		</member>
+		<member name="marker_id" type="int" setter="set_marker_id" getter="get_marker_id" default="0">
+			The marker ID for this marker, this is only returned for Aruco and April Tag markers. Call [method get_marker_data] for QRCode markers.
+		</member>
+		<member name="marker_type" type="int" setter="set_marker_type" getter="get_marker_type" enum="OpenXRSpatialComponentMarkerList.MarkerType" default="0">
+			The type of marker.
+		</member>
+	</members>
+</class>

+ 65 - 0
modules/openxr/doc_classes/OpenXRPlaneTracker.xml

@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRPlaneTracker" inherits="OpenXRSpatialEntityTracker" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Spatial entity tracker for our spatial entity plane tracking extension.
+	</brief_description>
+	<description>
+		Spatial entity tracker for our OpenXR spatial entity plane tracking extension. These trackers identify entities in our real space such as walls, floors, tables, etc. and map their location to our virtual space.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="clear_mesh_data">
+			<return type="void" />
+			<description>
+				Clears the mesh data for this tracker. You should only call this if you are handling your own discovery logic.
+			</description>
+		</method>
+		<method name="get_mesh">
+			<return type="Mesh" />
+			<description>
+				Gets a mesh created from either the mesh data or from our bounding size for this plane.
+			</description>
+		</method>
+		<method name="get_mesh_offset" qualifiers="const">
+			<return type="Transform3D" />
+			<description>
+				Gets the transform by which to offset the mesh and collision shape from our pose to display these correctly.
+			</description>
+		</method>
+		<method name="get_shape">
+			<return type="Shape3D" />
+			<param index="0" name="thickness" type="float" default="0.01" />
+			<description>
+				Gets a collision shape built either from the mesh data or from our bounding size for this plane.
+			</description>
+		</method>
+		<method name="set_mesh_data">
+			<return type="void" />
+			<param index="0" name="origin" type="Transform3D" />
+			<param index="1" name="vertices" type="PackedVector2Array" />
+			<param index="2" name="indices" type="PackedInt32Array" default="PackedInt32Array()" />
+			<description>
+				Sets the mesh data for this plane. You should only call this if you are handling your own discovery logic.
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="bounds_size" type="Vector2" setter="set_bounds_size" getter="get_bounds_size" default="Vector2(0, 0)">
+			The bounding size of the plane. This is a 2D size.
+		</member>
+		<member name="plane_alignment" type="int" setter="set_plane_alignment" getter="get_plane_alignment" enum="OpenXRSpatialComponentPlaneAlignmentList.PlaneAlignment" default="0">
+			The main alignment in space of this plane.
+		</member>
+		<member name="plane_label" type="String" setter="set_plane_label" getter="get_plane_label" default="&quot;&quot;">
+			The semantic label for this plane.
+		</member>
+	</members>
+	<signals>
+		<signal name="mesh_changed">
+			<description>
+				Emitted when our mesh data has changed the mesh instance and collision needs to be updated.
+			</description>
+		</signal>
+	</signals>
+</class>

+ 100 - 0
modules/openxr/doc_classes/OpenXRSpatialAnchorCapability.xml

@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialAnchorCapability" inherits="OpenXRExtensionWrapper" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Implementation for handling spatial entity anchor logic.
+	</brief_description>
+	<description>
+		This is an internal class that handles the OpenXR anchor spatial entity extension.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="create_new_anchor">
+			<return type="OpenXRAnchorTracker" />
+			<param index="0" name="transform" type="Transform3D" />
+			<param index="1" name="spatial_context" type="RID" default="RID()" />
+			<description>
+				Creates a new anchor that will be tracked by the XR runtime. The [param transform] should be a transform in the local space of your [XROrigin3D] node. If [param spatial_context] is not specified the default will be used, this requires [member ProjectSettings.xr/openxr/extensions/spatial_entity/enable_builtin_anchor_detection] to be set. The returned tracker will track the location in case our reference space changes.
+			</description>
+		</method>
+		<method name="create_persistence_context">
+			<return type="OpenXRFutureResult" />
+			<param index="0" name="scope" type="int" enum="OpenXRSpatialAnchorCapability.PersistenceScope" />
+			<param index="1" name="user_callback" type="Callable" default="Callable()" />
+			<description>
+				Creates a new persistence context for storing persistent data.
+				[b]Note:[/b] This is an asynchronous method and returns an [OpenXRFutureResult] object with which to track the status, discarding this object will not cancel the creation process. On success [param user_callback] will be called if specified. The result value for this function is the [RID] for our persistence context.
+			</description>
+		</method>
+		<method name="free_persistence_context">
+			<return type="void" />
+			<param index="0" name="persistence_context" type="RID" />
+			<description>
+				Frees a persistence context previously created with [method create_persistence_context].
+			</description>
+		</method>
+		<method name="get_persistence_context_handle" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="persistence_context" type="RID" />
+			<description>
+				Returns the internal handle for this persistence context.
+				[b]Note:[/b] For GDExtension implementations.
+			</description>
+		</method>
+		<method name="is_persistence_scope_supported">
+			<return type="bool" />
+			<param index="0" name="scope" type="int" enum="OpenXRSpatialAnchorCapability.PersistenceScope" />
+			<description>
+				Returns [code]true[/code] if this persistence scope is supported by our spatial anchor capability.
+				[b]Note:[/b] Only valid after an OpenXR instance has been created.
+			</description>
+		</method>
+		<method name="is_spatial_anchor_supported">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if spatial anchors are supported by the hardware. Only returns a valid value after OpenXR has been initialized.
+			</description>
+		</method>
+		<method name="is_spatial_persistence_supported">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if persistent spatial anchors are supported by the hardware. Only returns a valid value after OpenXR has been initialized.
+			</description>
+		</method>
+		<method name="persist_anchor">
+			<return type="OpenXRFutureResult" />
+			<param index="0" name="anchor_tracker" type="OpenXRAnchorTracker" />
+			<param index="1" name="persistence_context" type="RID" default="RID()" />
+			<param index="2" name="user_callback" type="Callable" default="Callable()" />
+			<description>
+				Changes this anchor into a persistent anchor. This means its location will be stored on the device and the anchor will be restored the next time your application starts. If [param persistence_context] is not specified the default will be used, this requires [member ProjectSettings.xr/openxr/extensions/spatial_entity/enable_builtin_anchor_detection] to be set.
+				[b]Note:[/b] This is an asynchronous method and returns an [OpenXRFutureResult] object with which to track the status, discarding this object will not cancel the creation process. On success [param user_callback] will be called if specified. The result value for this function is a boolean which will be set to [code]true[/code] on successful completion.
+			</description>
+		</method>
+		<method name="remove_anchor">
+			<return type="void" />
+			<param index="0" name="anchor_tracker" type="OpenXRAnchorTracker" />
+			<description>
+				Remove an anchor previously created with [method create_new_anchor]. If this anchor was persistent you must first call [method unpersist_anchor] and await its callback.
+			</description>
+		</method>
+		<method name="unpersist_anchor">
+			<return type="OpenXRFutureResult" />
+			<param index="0" name="anchor_tracker" type="OpenXRAnchorTracker" />
+			<param index="1" name="persistence_context" type="RID" default="RID()" />
+			<param index="2" name="user_callback" type="Callable" default="Callable()" />
+			<description>
+				Removes the persistent data from this anchor. The runtime will not recreate the anchor when your application restarts. If [param persistence_context] is not specified the default will be used, this requires [member ProjectSettings.xr/openxr/extensions/spatial_entity/enabled] to be set.
+				[b]Note:[/b] This is an asynchronous method and returns an [OpenXRFutureResult] object with which to track the status, discarding this object will not cancel the creation process. On success [param user_callback] will be called if specified. The result value for this function is a boolean which will be set to [code]true[/code] on successful completion.
+			</description>
+		</method>
+	</methods>
+	<constants>
+		<constant name="PERSISTENCE_SCOPE_SYSTEM_MANAGED" value="1" enum="PersistenceScope">
+			Provides the application with read-only access (i.e. application cannot modify this scope) to spatial entities persisted and managed by the system. The application can use the UUID in the persistence component for this scope to correlate entities across spatial contexts and device reboots.
+		</constant>
+		<constant name="PERSISTENCE_SCOPE_LOCAL_ANCHORS" value="1000781000" enum="PersistenceScope">
+			Persistence operations and data access is limited to spatial anchors, on the same device, for the same user and same app (using [method persist_anchor] and [method unpersist_anchor] functions)
+		</constant>
+	</constants>
+</class>

+ 20 - 0
modules/openxr/doc_classes/OpenXRSpatialCapabilityConfigurationAnchor.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialCapabilityConfigurationAnchor" inherits="OpenXRSpatialCapabilityConfigurationBaseHeader" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Configuration header for spatial anchors.
+	</brief_description>
+	<description>
+		Configuration header for spatial anchors. Pass this to [method OpenXRSpatialEntityExtension.create_spatial_context] to create a spatial context with spatial anchor capabilities.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_enabled_components" qualifiers="const">
+			<return type="PackedInt64Array" />
+			<description>
+				Returns the components enabled by this configuration.
+				[b]Note:[/b] Only valid after this configuration was used to create a spatial context.
+			</description>
+		</method>
+	</methods>
+</class>

+ 40 - 0
modules/openxr/doc_classes/OpenXRSpatialCapabilityConfigurationAprilTag.xml

@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialCapabilityConfigurationAprilTag" inherits="OpenXRSpatialCapabilityConfigurationBaseHeader" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Configuration header for April tag markers.
+	</brief_description>
+	<description>
+		Configuration header for April tag markers. Pass this to [method OpenXRSpatialEntityExtension.create_spatial_context] to create a spatial context that can detect April tags.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_enabled_components" qualifiers="const">
+			<return type="PackedInt64Array" />
+			<description>
+				Returns the components enabled by this configuration.
+				[b]Note:[/b] Only valid after this configuration was used to create a spatial context.
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="april_dict" type="int" setter="set_april_dict" getter="get_april_dict" enum="OpenXRSpatialCapabilityConfigurationAprilTag.AprilTagDict" default="4">
+			Dictionary to use to decode April tags.
+			[b]Note:[/b] Must be set before using this configuration to create a spatial context.
+		</member>
+	</members>
+	<constants>
+		<constant name="APRIL_TAG_DICT_16H5" value="1" enum="AprilTagDict">
+			4 by 4 bits, minimum Hamming distance between any two codes = 5, 30 codes.
+		</constant>
+		<constant name="APRIL_TAG_DICT_25H9" value="2" enum="AprilTagDict">
+			5 by 5 bits, minimum Hamming distance between any two codes = 9, 35 codes.
+		</constant>
+		<constant name="APRIL_TAG_DICT_36H10" value="3" enum="AprilTagDict">
+			 6 by 6 bits, minimum Hamming distance between any two codes = 10, 2320 codes.
+		</constant>
+		<constant name="APRIL_TAG_DICT_36H11" value="4" enum="AprilTagDict">
+			6 by 6 bits, minimum Hamming distance between any two codes = 11, 587 codes.
+		</constant>
+	</constants>
+</class>

+ 76 - 0
modules/openxr/doc_classes/OpenXRSpatialCapabilityConfigurationAruco.xml

@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialCapabilityConfigurationAruco" inherits="OpenXRSpatialCapabilityConfigurationBaseHeader" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Configuration header for Aruco markers.
+	</brief_description>
+	<description>
+		Configuration header for Aruco markers. Pass this to [method OpenXRSpatialEntityExtension.create_spatial_context] to create a spatial context that can detect Aruco markers.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_enabled_components" qualifiers="const">
+			<return type="PackedInt64Array" />
+			<description>
+				Returns the components enabled by this configuration.
+				[b]Note:[/b] Only valid after this configuration was used to create a spatial context.
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="aruco_dict" type="int" setter="set_aruco_dict" getter="get_aruco_dict" enum="OpenXRSpatialCapabilityConfigurationAruco.ArucoDict" default="16">
+			Dictionary to use to decode Aruco markers.
+			[b]Note:[/b] Must be set before using this configuration to create a spatial context.
+		</member>
+	</members>
+	<constants>
+		<constant name="ARUCO_DICT_4X4_50" value="1" enum="ArucoDict">
+			4 by 4 pixel Aruco marker dictionary with 50 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_4X4_100" value="2" enum="ArucoDict">
+			4 by 4 pixel Aruco marker dictionary with 100 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_4X4_250" value="3" enum="ArucoDict">
+			4 by 4 pixel Aruco marker dictionary with 250 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_4X4_1000" value="4" enum="ArucoDict">
+			4 by 4 pixel Aruco marker dictionary with 1000 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_5X5_50" value="5" enum="ArucoDict">
+			5 by 5 pixel Aruco marker dictionary with 50 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_5X5_100" value="6" enum="ArucoDict">
+			5 by 5 pixel Aruco marker dictionary with 100 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_5X5_250" value="7" enum="ArucoDict">
+			5 by 5 pixel Aruco marker dictionary with 250 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_5X5_1000" value="8" enum="ArucoDict">
+			5 by 5 pixel Aruco marker dictionary with 1000 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_6X6_50" value="9" enum="ArucoDict">
+			6 by 6 pixel Aruco marker dictionary with 50 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_6X6_100" value="10" enum="ArucoDict">
+			6 by 6 pixel Aruco marker dictionary with 100 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_6X6_250" value="11" enum="ArucoDict">
+			6 by 6 pixel Aruco marker dictionary with 250 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_6X6_1000" value="12" enum="ArucoDict">
+			6 by 6 pixel Aruco marker dictionary with 1000 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_7X7_50" value="13" enum="ArucoDict">
+			7 by 7 pixel Aruco marker dictionary with 50 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_7X7_100" value="14" enum="ArucoDict">
+			7 by 7 pixel Aruco marker dictionary with 100 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_7X7_250" value="15" enum="ArucoDict">
+			7 by 7 pixel Aruco marker dictionary with 250 IDs.
+		</constant>
+		<constant name="ARUCO_DICT_7X7_1000" value="16" enum="ArucoDict">
+			7 by 7 pixel Aruco marker dictionary with 1000 IDs.
+		</constant>
+	</constants>
+</class>

+ 31 - 0
modules/openxr/doc_classes/OpenXRSpatialCapabilityConfigurationBaseHeader.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialCapabilityConfigurationBaseHeader" inherits="RefCounted" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Wrapper base class for OpenXR Spatial Capability Configuration headers.
+	</brief_description>
+	<description>
+		Wrapper base class for OpenXR Spatial Capability Configuration headers. This class needs to be implemented for each capability configuration structure usable within OpenXR's spatial entities system.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="_get_configuration" qualifiers="virtual">
+			<return type="int" />
+			<description>
+				Return a pointer (encoded as an [code]int64_t[/code]) to a struct holding the spatial capability configuration data. The memory for this struct should remain accessible as long as this object remains instantiated.
+			</description>
+		</method>
+		<method name="_has_valid_configuration" qualifiers="virtual const">
+			<return type="bool" />
+			<description>
+				Return [code]true[/code] if this object contains a valid configuration that can be retrieved when calling [method _get_configuration].
+			</description>
+		</method>
+		<method name="has_valid_configuration" qualifiers="const">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if this object contains a valid configuration that can be used when calling [method OpenXRSpatialEntityExtension.create_spatial_context].
+			</description>
+		</method>
+	</methods>
+</class>

+ 20 - 0
modules/openxr/doc_classes/OpenXRSpatialCapabilityConfigurationMicroQrCode.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialCapabilityConfigurationMicroQrCode" inherits="OpenXRSpatialCapabilityConfigurationBaseHeader" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Configuration header for QR code markers.
+	</brief_description>
+	<description>
+		Configuration header for QR code markers. Pass this to [method OpenXRSpatialEntityExtension.create_spatial_context] to create a spatial context that can detect QR code markers.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_enabled_components" qualifiers="const">
+			<return type="PackedInt64Array" />
+			<description>
+				Returns the components enabled by this configuration.
+				[b]Note:[/b] Only valid after this configuration was used to create a spatial context.
+			</description>
+		</method>
+	</methods>
+</class>

+ 38 - 0
modules/openxr/doc_classes/OpenXRSpatialCapabilityConfigurationPlaneTracking.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialCapabilityConfigurationPlaneTracking" inherits="OpenXRSpatialCapabilityConfigurationBaseHeader" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Configuration header for plane tracking.
+	</brief_description>
+	<description>
+		Configuration header for plane tracking. Pass this to [method OpenXRSpatialEntityExtension.create_spatial_context] to create a spatial context with plane tracking capabilities.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_enabled_components" qualifiers="const">
+			<return type="PackedInt64Array" />
+			<description>
+				Returns the components enabled by this configuration.
+				[b]Note:[/b] Only valid after this configuration was used to create a spatial context.
+			</description>
+		</method>
+		<method name="supports_labels">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if we support the plane semantic label component (only valid after the OpenXR session has started). You can query these using the [OpenXRSpatialComponentPlaneSemanticLabelList] data object.
+			</description>
+		</method>
+		<method name="supports_mesh_2d">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if we support the mesh 2D component (only valid after the OpenXR session has started). You can query these using the [OpenXRSpatialComponentMesh2DList] data object.
+			</description>
+		</method>
+		<method name="supports_polygons">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if we support the polygon 2D component (only valid after the OpenXR session has started). You can query these using the [OpenXRSpatialComponentPolygon2DList] data object.
+			</description>
+		</method>
+	</methods>
+</class>

+ 20 - 0
modules/openxr/doc_classes/OpenXRSpatialCapabilityConfigurationQrCode.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialCapabilityConfigurationQrCode" inherits="OpenXRSpatialCapabilityConfigurationBaseHeader" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Configuration header for micro QR code markers.
+	</brief_description>
+	<description>
+		Configuration header for micro QR code markers. Pass this to [method OpenXRSpatialEntityExtension.create_spatial_context] to create a spatial context that can detect micro QR code markers.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_enabled_components" qualifiers="const">
+			<return type="PackedInt64Array" />
+			<description>
+				Returns the components enabled by this configuration.
+				[b]Note:[/b] Only valid after this configuration was used to create a spatial context.
+			</description>
+		</method>
+	</methods>
+</class>

+ 20 - 0
modules/openxr/doc_classes/OpenXRSpatialComponentAnchorList.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialComponentAnchorList" inherits="OpenXRSpatialComponentData" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Object for storing the queries anchor result data.
+	</brief_description>
+	<description>
+		Object for storing the queries anchor result data when calling [method OpenXRSpatialEntityExtension.query_snapshot].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_entity_pose" qualifiers="const">
+			<return type="Transform3D" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the transform for the entity at this [param index].
+			</description>
+		</method>
+	</methods>
+</class>

+ 27 - 0
modules/openxr/doc_classes/OpenXRSpatialComponentBounded2DList.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialComponentBounded2DList" inherits="OpenXRSpatialComponentData" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Object for storing the queries bounded2d result data.
+	</brief_description>
+	<description>
+		Object for storing the queries 2D bounding rectangle result data when calling [method OpenXRSpatialEntityExtension.query_snapshot].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_center_pose" qualifiers="const">
+			<return type="Transform3D" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the center of our bounding rectangle for the entity at this [param index].
+			</description>
+		</method>
+		<method name="get_size" qualifiers="const">
+			<return type="Vector2" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the size of our bounding rectangle for the entity at this [param index].
+			</description>
+		</method>
+	</methods>
+</class>

+ 27 - 0
modules/openxr/doc_classes/OpenXRSpatialComponentBounded3DList.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialComponentBounded3DList" inherits="OpenXRSpatialComponentData" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Object for storing the queries bounded3d result data.
+	</brief_description>
+	<description>
+		Object for storing the queries 3d bounding box result data when calling [method OpenXRSpatialEntityExtension.query_snapshot].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_center_pose" qualifiers="const">
+			<return type="Transform3D" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the center of our bounding box for the entity at this [param index].
+			</description>
+		</method>
+		<method name="get_size" qualifiers="const">
+			<return type="Vector3" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the size of our bounding box for the entity at this [param index].
+			</description>
+		</method>
+	</methods>
+</class>

+ 40 - 0
modules/openxr/doc_classes/OpenXRSpatialComponentData.xml

@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialComponentData" inherits="RefCounted" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Object for storing OpenXR spatial entity component data.
+	</brief_description>
+	<description>
+		Object for storing OpenXR spatial entity component data.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="_get_component_type" qualifiers="virtual const">
+			<return type="int" />
+			<description>
+				Return the component type for the component we store data for.
+			</description>
+		</method>
+		<method name="_get_structure_data" qualifiers="virtual const">
+			<return type="int" />
+			<param index="0" name="next" type="int" />
+			<description>
+				Return a pointer to the structure data that will be submitted along with the snapshot query. This pointer must remain valid as long as this object is instantiated.
+			</description>
+		</method>
+		<method name="_set_capacity" qualifiers="virtual">
+			<return type="void" />
+			<param index="0" name="capacity" type="int" />
+			<description>
+				Set the expected capacity as provided by the spatial entities query system. Buffers should be initialized with the correct storage.
+			</description>
+		</method>
+		<method name="set_capacity">
+			<return type="void" />
+			<param index="0" name="capacity" type="int" />
+			<description>
+				Set the expected capacity as provided by the spatial entities query system. Buffers should be initialized with the correct storage.
+			</description>
+		</method>
+	</methods>
+</class>

+ 55 - 0
modules/openxr/doc_classes/OpenXRSpatialComponentMarkerList.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialComponentMarkerList" inherits="OpenXRSpatialComponentData" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Object for storing the queries marker result data.
+	</brief_description>
+	<description>
+		Object for storing the queries marker result data when calling [method OpenXRSpatialEntityExtension.query_snapshot].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_marker_data" qualifiers="const">
+			<return type="Variant" />
+			<param index="0" name="snapshot" type="RID" />
+			<param index="1" name="index" type="int" />
+			<description>
+				Returns either a [String] or a [PackedByteArray] buffer with data for the marker at this [param index]. Only applicable for QR code markers.
+			</description>
+		</method>
+		<method name="get_marker_id" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the marker ID for the marker at this [param index]. Only applicable for Aruco or April Tag markers.
+			</description>
+		</method>
+		<method name="get_marker_type" qualifiers="const">
+			<return type="int" enum="OpenXRSpatialComponentMarkerList.MarkerType" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the marker type for the marker at this [param index].
+			</description>
+		</method>
+	</methods>
+	<constants>
+		<constant name="MARKER_TYPE_UNKNOWN" value="0" enum="MarkerType">
+			Unknown or unset marker type.
+		</constant>
+		<constant name="MARKER_TYPE_QRCODE" value="1" enum="MarkerType">
+			Marker based on a QR code.
+		</constant>
+		<constant name="MARKER_TYPE_MICRO_QRCODE" value="2" enum="MarkerType">
+			Marker based on a micro QR code.
+		</constant>
+		<constant name="MARKER_TYPE_ARUCO" value="3" enum="MarkerType">
+			Marker based on an Aruco code.
+		</constant>
+		<constant name="MARKER_TYPE_APRIL_TAG" value="4" enum="MarkerType">
+			Marker based on an April Tag.
+		</constant>
+		<constant name="MARKER_TYPE_MAX" value="5" enum="MarkerType">
+			Maximum value for this enum.
+		</constant>
+	</constants>
+</class>

+ 36 - 0
modules/openxr/doc_classes/OpenXRSpatialComponentMesh2DList.xml

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialComponentMesh2DList" inherits="OpenXRSpatialComponentData" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Object for storing the queries mesh2d result data.
+	</brief_description>
+	<description>
+		Object for storing the queries 2D mesh result data when calling [method OpenXRSpatialEntityExtension.query_snapshot].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_indices" qualifiers="const">
+			<return type="PackedInt32Array" />
+			<param index="0" name="snapshot" type="RID" />
+			<param index="1" name="index" type="int" />
+			<description>
+				Returns the mesh indices for the entity at this [param index].
+			</description>
+		</method>
+		<method name="get_transform" qualifiers="const">
+			<return type="Transform3D" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the transform for positioning our mesh for the entity at this [param index].
+			</description>
+		</method>
+		<method name="get_vertices" qualifiers="const">
+			<return type="PackedVector2Array" />
+			<param index="0" name="snapshot" type="RID" />
+			<param index="1" name="index" type="int" />
+			<description>
+				Returns the mesh vertices for the entity at this [param index].
+			</description>
+		</method>
+	</methods>
+</class>

+ 27 - 0
modules/openxr/doc_classes/OpenXRSpatialComponentMesh3DList.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialComponentMesh3DList" inherits="OpenXRSpatialComponentData" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Object for storing the queries mesh3d result data.
+	</brief_description>
+	<description>
+		Object for storing the queries 3d mesh result data when calling [method OpenXRSpatialEntityExtension.query_snapshot].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_mesh" qualifiers="const">
+			<return type="Mesh" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the mesh for the entity at this [param index].
+			</description>
+		</method>
+		<method name="get_transform" qualifiers="const">
+			<return type="Transform3D" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the transform for positioning our mesh for the entity at this [param index].
+			</description>
+		</method>
+	</methods>
+</class>

+ 20 - 0
modules/openxr/doc_classes/OpenXRSpatialComponentParentList.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialComponentParentList" inherits="OpenXRSpatialComponentData" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Object for storing the queries parent result data.
+	</brief_description>
+	<description>
+		Object for storing the queries parent result data when calling [method OpenXRSpatialEntityExtension.query_snapshot].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_parent" qualifiers="const">
+			<return type="RID" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the RID for the parent entity at this [param index].
+			</description>
+		</method>
+	</methods>
+</class>

+ 27 - 0
modules/openxr/doc_classes/OpenXRSpatialComponentPersistenceList.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialComponentPersistenceList" inherits="OpenXRSpatialComponentData" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Object for storing the query persistence result data.
+	</brief_description>
+	<description>
+		Object for storing the query persistence result data when calling [method OpenXRSpatialEntityExtension.query_snapshot].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_persistent_state" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the persistent state ([code]XrSpatialPersistenceStateEXT[/code]) for the entity at this [param index].
+			</description>
+		</method>
+		<method name="get_persistent_uuid" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the persistent uuid for the entity at this [param index].
+			</description>
+		</method>
+	</methods>
+</class>

+ 34 - 0
modules/openxr/doc_classes/OpenXRSpatialComponentPlaneAlignmentList.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialComponentPlaneAlignmentList" inherits="OpenXRSpatialComponentData" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Object for storing the queries plane alignment result data.
+	</brief_description>
+	<description>
+		Object for storing the queries plane alignment result data when calling [method OpenXRSpatialEntityExtension.query_snapshot].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_plane_alignment" qualifiers="const">
+			<return type="int" enum="OpenXRSpatialComponentPlaneAlignmentList.PlaneAlignment" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the plane alignment for the parent entity at this [param index].
+			</description>
+		</method>
+	</methods>
+	<constants>
+		<constant name="PLANE_ALIGNMENT_HORIZONTAL_UPWARD" value="0" enum="PlaneAlignment">
+			Plane is facing upward.
+		</constant>
+		<constant name="PLANE_ALIGNMENT_HORIZONTAL_DOWNWARD" value="1" enum="PlaneAlignment">
+			Plane is facing downwards.
+		</constant>
+		<constant name="PLANE_ALIGNMENT_VERTICAL" value="2" enum="PlaneAlignment">
+			Plane is vertically aligned.
+		</constant>
+		<constant name="PLANE_ALIGNMENT_ARBITRARY" value="3" enum="PlaneAlignment">
+			Plane has an arbitrary alignment.
+		</constant>
+	</constants>
+</class>

+ 37 - 0
modules/openxr/doc_classes/OpenXRSpatialComponentPlaneSemanticLabelList.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialComponentPlaneSemanticLabelList" inherits="OpenXRSpatialComponentData" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Object for storing the queries plane semantic label result data.
+	</brief_description>
+	<description>
+		Object for storing the queries plane semantic label result data when calling [method OpenXRSpatialEntityExtension.query_snapshot].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_plane_semantic_label" qualifiers="const">
+			<return type="int" enum="OpenXRSpatialComponentPlaneSemanticLabelList.PlaneSemanticLabel" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the plane semantic label for the parent entity at this [param index].
+			</description>
+		</method>
+	</methods>
+	<constants>
+		<constant name="PLANE_SEMANTIC_LABEL_UNCATEGORIZED" value="1" enum="PlaneSemanticLabel">
+			Uncategorized plane.
+		</constant>
+		<constant name="PLANE_SEMANTIC_LABEL_FLOOR" value="2" enum="PlaneSemanticLabel">
+			Plane represents a floor.
+		</constant>
+		<constant name="PLANE_SEMANTIC_LABEL_WALL" value="3" enum="PlaneSemanticLabel">
+			Plane represents a wall.
+		</constant>
+		<constant name="PLANE_SEMANTIC_LABEL_CEILING" value="4" enum="PlaneSemanticLabel">
+			Plane represents a ceiling.
+		</constant>
+		<constant name="PLANE_SEMANTIC_LABEL_TABLE" value="5" enum="PlaneSemanticLabel">
+			Plane represents the surface of a table.
+		</constant>
+	</constants>
+</class>

+ 28 - 0
modules/openxr/doc_classes/OpenXRSpatialComponentPolygon2DList.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialComponentPolygon2DList" inherits="OpenXRSpatialComponentData" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Object for storing the queries polygon2d result data.
+	</brief_description>
+	<description>
+		Object for storing the queries 2D polygon result data when calling [method OpenXRSpatialEntityExtension.query_snapshot].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_transform" qualifiers="const">
+			<return type="Transform3D" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the transform for positioning our polygon for the entity at this [param index].
+			</description>
+		</method>
+		<method name="get_vertices" qualifiers="const">
+			<return type="PackedVector2Array" />
+			<param index="0" name="snapshot" type="RID" />
+			<param index="1" name="index" type="int" />
+			<description>
+				Returns the polygon vertices for the entity at this [param index].
+			</description>
+		</method>
+	</methods>
+</class>

+ 27 - 0
modules/openxr/doc_classes/OpenXRSpatialContextPersistenceConfig.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialContextPersistenceConfig" inherits="OpenXRStructureBase" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Configuration header for spatial persistence.
+	</brief_description>
+	<description>
+		Configuration header for spatial persistence. Pass this to [method OpenXRSpatialEntityExtension.create_spatial_context] as the next parameter to create a spatial context with spatial persistence capabilities.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="add_persistence_context">
+			<return type="void" />
+			<param index="0" name="persistence_context" type="RID" />
+			<description>
+				Adds a persistence context to this configuration. You must add at least one persistence context to create a valid configuration. You can create a persistence context by calling [method OpenXRSpatialAnchorCapability.create_persistence_context].
+			</description>
+		</method>
+		<method name="remove_persistence_context">
+			<return type="void" />
+			<param index="0" name="persistence_context" type="RID" />
+			<description>
+				Removes a persistence context.
+			</description>
+		</method>
+	</methods>
+</class>

+ 277 - 0
modules/openxr/doc_classes/OpenXRSpatialEntityExtension.xml

@@ -0,0 +1,277 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialEntityExtension" inherits="OpenXRExtensionWrapper" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		OpenXR extension that handles spatial entities.
+	</brief_description>
+	<description>
+		OpenXR extension that handles spatial entities and, when enabled, allows querying those spatial entities. This extension will also automatically manage [XRTracker] objects for static entities.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="add_spatial_entity">
+			<return type="RID" />
+			<param index="0" name="spatial_context" type="RID" />
+			<param index="1" name="entity_id" type="int" />
+			<param index="2" name="entity" type="int" />
+			<description>
+				Registers an entity that was created directly on the OpenXR runtime.
+			</description>
+		</method>
+		<method name="create_spatial_context">
+			<return type="OpenXRFutureResult" />
+			<param index="0" name="capability_configurations" type="OpenXRSpatialCapabilityConfigurationBaseHeader[]" />
+			<param index="1" name="next" type="OpenXRStructureBase" default="null" />
+			<param index="2" name="user_callback" type="Callable" default="Callable()" />
+			<description>
+				Creates a new spatial context that handles entities for the provided capability configurations. [param capability_configurations] is an array of [OpenXRSpatialCapabilityConfigurationBaseHeader] with the needed capability configuration data.
+				[param next] is an optional parameter that can contain additional information for creating our spatial context.
+				[b]Note:[/b] This is an asynchronous method and returns an [OpenXRFutureResult] object with which to track the status, discarding this object will not cancel the creation process. On success [param user_callback] will be called if specified. The result data for this function is the [RID] for our spatial context.
+			</description>
+		</method>
+		<method name="discover_spatial_entities">
+			<return type="OpenXRFutureResult" />
+			<param index="0" name="spatial_context" type="RID" />
+			<param index="1" name="component_types" type="PackedInt64Array" />
+			<param index="2" name="next" type="OpenXRStructureBase" default="null" />
+			<param index="3" name="user_callback" type="Callable" default="Callable()" />
+			<description>
+				Starts a new discovery query, this will gather all objects tracked by the [param spatial_context] that have at least one of the component types specified in [param component_types].
+				[param next] is an optional parameter that can contain additional information for executing the discovery query.
+				[b]Note:[/b] This is an asynchronous method and returns an [OpenXRFutureResult] object with which to track the status, discarding this object will not cancel the discovery process. On success [param user_callback] will be called if specified. The result data for this function is the [RID] for our snapshot.
+			</description>
+		</method>
+		<method name="find_spatial_entity">
+			<return type="RID" />
+			<param index="0" name="entity_id" type="int" />
+			<description>
+				Returns the [RID] for the specified spatial entity ID.
+			</description>
+		</method>
+		<method name="free_spatial_context">
+			<return type="void" />
+			<param index="0" name="spatial_context" type="RID" />
+			<description>
+				Frees a spatial context previously created when calling [method create_spatial_context]. If the spatial context creation is still ongoing, the asynchronous process is cancelled.
+			</description>
+		</method>
+		<method name="free_spatial_entity">
+			<return type="void" />
+			<param index="0" name="entity" type="RID" />
+			<description>
+				Frees an entity previously created when calling [method add_spatial_entity] or [method make_spatial_entity].
+			</description>
+		</method>
+		<method name="free_spatial_snapshot">
+			<return type="void" />
+			<param index="0" name="spatial_snapshot" type="RID" />
+			<description>
+				Frees a spatial snapshot previously created when calling [method discover_spatial_entities]. If the spatial snapshot creation is still ongoing, the asynchronous process is cancelled.
+			</description>
+		</method>
+		<method name="get_float_buffer" qualifiers="const">
+			<return type="PackedFloat32Array" />
+			<param index="0" name="spatial_snapshot" type="RID" />
+			<param index="1" name="buffer_id" type="int" />
+			<description>
+				Returns a buffer with floats from a buffer that was retrieved when taking a snapshot.
+			</description>
+		</method>
+		<method name="get_spatial_context_handle" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="spatial_context" type="RID" />
+			<description>
+				Returns the OpenXR spatial context handle for this snapshot.
+				[b]Note:[/b] This method is intended to be used from GDExtensions that implement spatial entity capability handlers.
+			</description>
+		</method>
+		<method name="get_spatial_context_ready" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="spatial_context" type="RID" />
+			<description>
+				Returns [code]true[/code] if the spatial context finished its creation and is ready to be used.
+			</description>
+		</method>
+		<method name="get_spatial_entity_context" qualifiers="const">
+			<return type="RID" />
+			<param index="0" name="entity" type="RID" />
+			<description>
+				Returns the spatial context for this entity.
+			</description>
+		</method>
+		<method name="get_spatial_entity_id" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="entity" type="RID" />
+			<description>
+				Returns the internal [code]XrSpatialEntityIdEXT[/code] associated with the entity.
+			</description>
+		</method>
+		<method name="get_spatial_snapshot_context" qualifiers="const">
+			<return type="RID" />
+			<param index="0" name="spatial_snapshot" type="RID" />
+			<description>
+				Returns the spatial context related to this spatial snapshot.
+			</description>
+		</method>
+		<method name="get_spatial_snapshot_handle" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="spatial_snapshot" type="RID" />
+			<description>
+				Returns the OpenXR spatial snapshot handle for this snapshot.
+				[b]Note:[/b] This method is intended to be used from GDExtensions that implement spatial entity capability handlers.
+			</description>
+		</method>
+		<method name="get_string" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="spatial_snapshot" type="RID" />
+			<param index="1" name="buffer_id" type="int" />
+			<description>
+				Returns a string from a buffer that was retrieved when taking a snapshot.
+			</description>
+		</method>
+		<method name="get_uint8_buffer" qualifiers="const">
+			<return type="PackedByteArray" />
+			<param index="0" name="spatial_snapshot" type="RID" />
+			<param index="1" name="buffer_id" type="int" />
+			<description>
+				Returns a buffer with 8 bit ints from a buffer that was retrieved when taking a snapshot.
+			</description>
+		</method>
+		<method name="get_uint16_buffer" qualifiers="const">
+			<return type="PackedInt32Array" />
+			<param index="0" name="spatial_snapshot" type="RID" />
+			<param index="1" name="buffer_id" type="int" />
+			<description>
+				Returns a buffer with 16 bit ints from a buffer that was retrieved when taking a snapshot.
+			</description>
+		</method>
+		<method name="get_uint32_buffer" qualifiers="const">
+			<return type="PackedInt32Array" />
+			<param index="0" name="spatial_snapshot" type="RID" />
+			<param index="1" name="buffer_id" type="int" />
+			<description>
+				Returns a buffer with 32 bit ints from a buffer that was retrieved when taking a snapshot.
+			</description>
+		</method>
+		<method name="get_vector2_buffer" qualifiers="const">
+			<return type="PackedVector2Array" />
+			<param index="0" name="spatial_snapshot" type="RID" />
+			<param index="1" name="buffer_id" type="int" />
+			<description>
+				Returns a buffer with [Vector2] entries from a buffer that was retrieved when taking a snapshot.
+			</description>
+		</method>
+		<method name="get_vector3_buffer" qualifiers="const">
+			<return type="PackedVector3Array" />
+			<param index="0" name="spatial_snapshot" type="RID" />
+			<param index="1" name="buffer_id" type="int" />
+			<description>
+				Returns a buffer with [Vector3] entries from a buffer that was retrieved when taking a snapshot.
+			</description>
+		</method>
+		<method name="make_spatial_entity">
+			<return type="RID" />
+			<param index="0" name="spatial_context" type="RID" />
+			<param index="1" name="entity_id" type="int" />
+			<description>
+				Creates a new entity for this [param entity_id]. The [param spatial_context] should match the context that discovered the entity.
+			</description>
+		</method>
+		<method name="query_snapshot">
+			<return type="bool" />
+			<param index="0" name="spatial_snapshot" type="RID" />
+			<param index="1" name="component_data" type="OpenXRSpatialComponentData[]" />
+			<param index="2" name="next" type="OpenXRStructureBase" default="null" />
+			<description>
+				Queries the snapshot data. This will find all entities in the snapshot that contain all requested components in [param component_data]. The objects held within [param component_data] will then be populated with the queried data. [param component_data] must always have an object of [OpenXRSpatialQueryResultData] as the first entry.
+				[param next] is an optional parameter that can contain additional information passed when setting our query conditions.
+			</description>
+		</method>
+		<method name="supports_capability">
+			<return type="bool" />
+			<param index="0" name="capability" type="int" enum="OpenXRSpatialEntityExtension.Capability" />
+			<description>
+				Returns [code]true[/code] if this spatial entity [param capability] is supported by the hardware used.
+			</description>
+		</method>
+		<method name="supports_component_type">
+			<return type="bool" />
+			<param index="0" name="capability" type="int" enum="OpenXRSpatialEntityExtension.Capability" />
+			<param index="1" name="component_type" type="int" enum="OpenXRSpatialEntityExtension.ComponentType" />
+			<description>
+				Returns [code]true[/code] if this [param capability] supports the [param component_type].
+			</description>
+		</method>
+		<method name="update_spatial_entities">
+			<return type="RID" />
+			<param index="0" name="spatial_context" type="RID" />
+			<param index="1" name="entities" type="RID[]" />
+			<param index="2" name="component_types" type="PackedInt64Array" />
+			<param index="3" name="next" type="OpenXRStructureBase" default="null" />
+			<description>
+				Performs a snapshot for a limited number of entities. This is NOT an asynchronous method and will return the snapshot immediately.
+			</description>
+		</method>
+	</methods>
+	<signals>
+		<signal name="spatial_discovery_recommended">
+			<param index="0" name="spatial_context" type="RID" />
+			<description>
+				Emitted when OpenXR recommends running a discovery query because entities managed by this spatial context have (likely) changed.
+			</description>
+		</signal>
+	</signals>
+	<constants>
+		<constant name="CAPABILITY_PLANE_TRACKING" value="1000741000" enum="Capability">
+			Plane tracking capability.
+		</constant>
+		<constant name="CAPABILITY_MARKER_TRACKING_QR_CODE" value="1000743000" enum="Capability">
+			QR code based marker tracking capability.
+		</constant>
+		<constant name="CAPABILITY_MARKER_TRACKING_MICRO_QR_CODE" value="1000743001" enum="Capability">
+			Micro QR code based marker tracking capability.
+		</constant>
+		<constant name="CAPABILITY_MARKER_TRACKING_ARUCO_MARKER" value="1000743002" enum="Capability">
+			Aruco marker based marker tracking capability.
+		</constant>
+		<constant name="CAPABILITY_MARKER_TRACKING_APRIL_TAG" value="1000743003" enum="Capability">
+			April tag based marker tracking capability.
+		</constant>
+		<constant name="CAPABILITY_ANCHOR" value="1000762000" enum="Capability">
+			Anchor capability.
+		</constant>
+		<constant name="COMPONENT_TYPE_BOUNDED_2D" value="1" enum="ComponentType">
+			Component that provides the 2D bounds for a spatial entity. The corresponding list structure is [code]XrSpatialComponentBounded2DListEXT[/code]; the corresponding data structure is [code]XrSpatialBounded2DDataEXT[/code].
+		</constant>
+		<constant name="COMPONENT_TYPE_BOUNDED_3D" value="2" enum="ComponentType">
+			Component that provides the 3D bounds for a spatial entity. The corresponding list structure is [code]XrSpatialComponentBounded3DListEXT[/code]; the corresponding data structure is [code]XrBoxf[/code].
+		</constant>
+		<constant name="COMPONENT_TYPE_PARENT" value="3" enum="ComponentType">
+			Component that provides the XrSpatialEntityIdEXT of the parent for a spatial entity. The corresponding list structure is [code]XrSpatialComponentParentListEXT[/code]; the corresponding data structure is [code]XrSpatialEntityIdEXT[/code].
+		</constant>
+		<constant name="COMPONENT_TYPE_MESH_3D" value="4" enum="ComponentType">
+			Component that provides a 3D mesh for a spatial entity. The corresponding list structure is [code]XrSpatialComponentMesh3DListEXT[/code]; the corresponding data structure is [code]XrSpatialMeshDataEXT[/code].
+		</constant>
+		<constant name="COMPONENT_TYPE_PLANE_ALIGNMENT" value="1000741000" enum="ComponentType">
+			Component that provides the plane alignment enum for a spatial entity. The corresponding list structure is [code]XrSpatialComponentPlaneAlignmentListEXT[/code]; the corresponding data structure is [code]XrSpatialPlaneAlignmentEXT[/code] (Added by the [code]XR_EXT_spatial_plane_tracking[/code] extension).
+		</constant>
+		<constant name="COMPONENT_TYPE_MESH_2D" value="1000741001" enum="ComponentType">
+			Component that provides a 2D mesh for a spatial entity. The corresponding list structure is [code]XrSpatialComponentMesh2DListEXT[/code]; the corresponding data structure is [code]XrSpatialMeshDataEXT[/code] (Added by the [code]XR_EXT_spatial_plane_tracking[/code] extension).
+		</constant>
+		<constant name="COMPONENT_TYPE_POLYGON_2D" value="1000741002" enum="ComponentType">
+			Component that provides a 2D boundary polygon for a spatial entity. The corresponding list structure is [code]XrSpatialComponentPolygon2DListEXT[/code]; the corresponding data structure is [code]XrSpatialPolygon2DDataEXT[/code] (Added by the [code]XR_EXT_spatial_plane_tracking[/code] extension).
+		</constant>
+		<constant name="COMPONENT_TYPE_PLANE_SEMANTIC_LABEL" value="1000741003" enum="ComponentType">
+			Component that provides a semantic label for a plane. The corresponding list structure is [code]XrSpatialComponentPlaneSemanticLabelListEXT[/code]; the corresponding data structure is [code]XrSpatialPlaneSemanticLabelEXT[/code] (Added by the [code]XR_EXT_spatial_plane_tracking[/code] extension).
+		</constant>
+		<constant name="COMPONENT_TYPE_MARKER" value="1000743000" enum="ComponentType">
+			A component describing the marker type, ID and location. The corresponding list structure is [code]XrSpatialComponentMarkerListEXT[/code]; the corresponding data structure is [code]XrSpatialMarkerDataEXT[/code] (Added by the [code]XR_EXT_spatial_marker_tracking[/code] extension).
+		</constant>
+		<constant name="COMPONENT_TYPE_ANCHOR" value="1000762000" enum="ComponentType">
+			Component that provides the location for an anchor. The corresponding list structure is [code]XrSpatialComponentAnchorListEXT[/code]; the corresponding data structure is [code]XrPosef[/code] (Added by the [code]XR_EXT_spatial_anchor[/code] extension).
+		</constant>
+		<constant name="COMPONENT_TYPE_PERSISTENCE" value="1000763000" enum="ComponentType">
+			Component that provides the persisted UUID for a spatial entity. The corresponding list structure is [code]XrSpatialComponentPersistenceListEXT; the corresponding data structure is [code]XrSpatialPersistenceDataEXT[/code] (Added by the [code]XR_EXT_spatial_persistence[/code] extension).
+		</constant>
+	</constants>
+</class>

+ 38 - 0
modules/openxr/doc_classes/OpenXRSpatialEntityTracker.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialEntityTracker" inherits="XRPositionalTracker" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Base class for Positional trackers managed by OpenXR's spatial entity extensions.
+	</brief_description>
+	<description>
+		These are trackers created and managed by OpenXR's spatial entity extensions that give access to specific data related to OpenXR's spatial entities. They will always be of type [code]TRACKER_ANCHOR[/code].
+	</description>
+	<tutorials>
+	</tutorials>
+	<members>
+		<member name="entity" type="RID" setter="set_entity" getter="get_entity" default="RID()">
+			The spatial entity associated with this tracker.
+		</member>
+		<member name="spatial_tracking_state" type="int" setter="set_spatial_tracking_state" getter="get_spatial_tracking_state" enum="OpenXRSpatialEntityTracker.EntityTrackingState" default="2">
+			The spatial tracking state for this tracker.
+		</member>
+		<member name="type" type="int" setter="set_tracker_type" getter="get_tracker_type" overrides="XRTracker" enum="XRServer.TrackerType" default="8" />
+	</members>
+	<signals>
+		<signal name="spatial_tracking_state_changed">
+			<param index="0" name="spatial_tracking_state" type="int" />
+			<description>
+			</description>
+		</signal>
+	</signals>
+	<constants>
+		<constant name="ENTITY_TRACKING_STATE_STOPPED" value="1" enum="EntityTrackingState">
+			This anchor has stopped tracking.
+		</constant>
+		<constant name="ENTITY_TRACKING_STATE_PAUSED" value="2" enum="EntityTrackingState">
+			Tracking is currently paused.
+		</constant>
+		<constant name="ENTITY_TRACKING_STATE_TRACKING" value="3" enum="EntityTrackingState">
+			This anchor is currently being tracked.
+		</constant>
+	</constants>
+</class>

+ 37 - 0
modules/openxr/doc_classes/OpenXRSpatialMarkerTrackingCapability.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialMarkerTrackingCapability" inherits="OpenXRExtensionWrapper" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Implementation for handling spatial entity marker tracking logic.
+	</brief_description>
+	<description>
+		This class handles the OpenXR marker tracking spatial entity extension.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="is_april_tag_supported">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if April tag marker tracking is supported by the current device.
+			</description>
+		</method>
+		<method name="is_aruco_supported">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if Aruco marker tracking is supported by the current device.
+			</description>
+		</method>
+		<method name="is_micro_qrcode_supported">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if micro QR code marker tracking is supported by the current device.
+			</description>
+		</method>
+		<method name="is_qrcode_supported">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if QR code marker tracking is supported by the current device.
+			</description>
+		</method>
+	</methods>
+</class>

+ 19 - 0
modules/openxr/doc_classes/OpenXRSpatialPlaneTrackingCapability.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialPlaneTrackingCapability" inherits="OpenXRExtensionWrapper" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Implementation for handling spatial entity plane tracking logic.
+	</brief_description>
+	<description>
+		This class handles the OpenXR plane tracking spatial entity extension.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="is_supported">
+			<return type="bool" />
+			<description>
+				Returns [code]true[/code] if plane tracking is supported by the current device.
+			</description>
+		</method>
+	</methods>
+</class>

+ 33 - 0
modules/openxr/doc_classes/OpenXRSpatialQueryResultData.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRSpatialQueryResultData" inherits="OpenXRSpatialComponentData" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Object for storing the main query result data.
+	</brief_description>
+	<description>
+		Object for storing the main query result data when calling [method OpenXRSpatialEntityExtension.query_snapshot]. This must always be the first component requested.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_capacity" qualifiers="const">
+			<return type="int" />
+			<description>
+				Returns the number of entities that were retrieved.
+			</description>
+		</method>
+		<method name="get_entity_id" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the entity id ([code]XrSpatialEntityIdEXT[/code]) for the entity at this [param index].
+			</description>
+		</method>
+		<method name="get_entity_state" qualifiers="const">
+			<return type="int" enum="OpenXRSpatialEntityTracker.EntityTrackingState" />
+			<param index="0" name="index" type="int" />
+			<description>
+				Returns the entity state for the entity at this [param index].
+			</description>
+		</method>
+	</methods>
+</class>

+ 30 - 0
modules/openxr/doc_classes/OpenXRStructureBase.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRStructureBase" inherits="RefCounted" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+	<brief_description>
+		Object for storing OpenXR structure data.
+	</brief_description>
+	<description>
+		Object for storing OpenXR structure data that is passed when calling into OpenXR APIs.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="_get_header" qualifiers="virtual">
+			<return type="int" />
+			<param index="0" name="next" type="int" />
+			<description>
+			</description>
+		</method>
+		<method name="get_structure_type">
+			<return type="int" />
+			<description>
+				Returns the structure type (OpenXR [code]XrStructureType[/code]) used for this structure.
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="next" type="OpenXRStructureBase" setter="set_next" getter="get_next">
+			Setting another structure object here chains these structures together to extend the API functionality. Consult the OpenXR documentation for which structures can be used with a given API call.
+		</member>
+	</members>
+</class>

+ 1 - 0
modules/openxr/extensions/SCsub

@@ -7,6 +7,7 @@ Import("env_openxr")
 module_obj = []
 
 env_openxr.add_source_files(module_obj, "*.cpp")
+env_openxr.add_source_files(module_obj, "spatial_entities/*.cpp")
 
 # These are platform dependent
 if env["platform"] == "android":

+ 1 - 1
modules/openxr/extensions/openxr_extension_wrapper.h

@@ -73,7 +73,7 @@ public:
 	// You should return the pointer to the last struct you define as your result.
 	// If you are not adding any structs, just return `p_next_pointer`.
 	// See existing extensions for examples of this implementation.
-	virtual void *set_system_properties_and_get_next_pointer(void *p_next_pointer); // Add additional data structures when we interrogate OpenXRS system abilities.
+	virtual void *set_system_properties_and_get_next_pointer(void *p_next_pointer); // Add additional data structures when we interrogate OpenXR's system abilities.
 	virtual void *set_instance_create_info_and_get_next_pointer(void *p_next_pointer); // Add additional data structures when we create our OpenXR instance.
 	virtual void *set_session_create_and_get_next_pointer(void *p_next_pointer); // Add additional data structures when we create our OpenXR session.
 	virtual void *set_swapchain_create_info_and_get_next_pointer(void *p_next_pointer); // Add additional data structures when creating OpenXR swap chains.

+ 1 - 1
modules/openxr/extensions/openxr_future_extension.h

@@ -31,7 +31,7 @@
 #pragma once
 
 /*
-	The OpenXR future extension forms the basis of OpenXRs ability to
+	The OpenXR future extension forms the basis of OpenXR's ability to
 	execute logic asynchronously.
 
 	Asynchronous functions will return a future object which can be

+ 1184 - 0
modules/openxr/extensions/spatial_entities/openxr_spatial_anchor.cpp

@@ -0,0 +1,1184 @@
+/**************************************************************************/
+/*  openxr_spatial_anchor.cpp                                             */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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 "openxr_spatial_anchor.h"
+
+#include "../../openxr_api.h"
+#include "../../openxr_util.h"
+#include "../openxr_future_extension.h"
+#include "core/config/project_settings.h"
+#include "servers/xr_server.h"
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialCapabilityConfigurationAnchor
+
+void OpenXRSpatialCapabilityConfigurationAnchor::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_enabled_components"), &OpenXRSpatialCapabilityConfigurationAnchor::_get_enabled_components);
+}
+
+bool OpenXRSpatialCapabilityConfigurationAnchor::has_valid_configuration() const {
+	OpenXRSpatialAnchorCapability *capability = OpenXRSpatialAnchorCapability::get_singleton();
+	ERR_FAIL_NULL_V(capability, false);
+
+	return capability->is_spatial_anchor_supported();
+}
+
+XrSpatialCapabilityConfigurationBaseHeaderEXT *OpenXRSpatialCapabilityConfigurationAnchor::get_configuration() {
+	OpenXRSpatialAnchorCapability *capability = OpenXRSpatialAnchorCapability::get_singleton();
+	ERR_FAIL_NULL_V(capability, nullptr);
+
+	if (capability->is_spatial_anchor_supported()) {
+		OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+		ERR_FAIL_NULL_V(se_extension, nullptr);
+
+		anchor_enabled_components.clear();
+
+		// Guaranteed components:
+		anchor_enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_ANCHOR_EXT);
+
+		// enable optional components
+		if (capability->is_spatial_persistence_supported()) {
+			anchor_enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_PERSISTENCE_EXT);
+		}
+
+		anchor_config.enabledComponentCount = anchor_enabled_components.size();
+		anchor_config.enabledComponents = anchor_enabled_components.ptr();
+
+		// and return this.
+		return (XrSpatialCapabilityConfigurationBaseHeaderEXT *)&anchor_config;
+	}
+
+	return nullptr;
+}
+
+PackedInt64Array OpenXRSpatialCapabilityConfigurationAnchor::_get_enabled_components() const {
+	PackedInt64Array components;
+
+	for (const XrSpatialComponentTypeEXT &component_type : anchor_enabled_components) {
+		components.push_back((int64_t)component_type);
+	}
+
+	return components;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialComponentAnchorList
+
+void OpenXRSpatialComponentAnchorList::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_entity_pose", "index"), &OpenXRSpatialComponentAnchorList::get_entity_pose);
+}
+
+void OpenXRSpatialComponentAnchorList::set_capacity(uint32_t p_capacity) {
+	entity_poses.resize(p_capacity);
+
+	anchor_list.locationCount = uint32_t(entity_poses.size());
+	anchor_list.locations = entity_poses.ptrw();
+}
+
+XrSpatialComponentTypeEXT OpenXRSpatialComponentAnchorList::get_component_type() const {
+	return XR_SPATIAL_COMPONENT_TYPE_ANCHOR_EXT;
+}
+
+void *OpenXRSpatialComponentAnchorList::get_structure_data(void *p_next) {
+	anchor_list.next = p_next;
+	return &anchor_list;
+}
+
+Transform3D OpenXRSpatialComponentAnchorList::get_entity_pose(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, entity_poses.size(), Transform3D());
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, Transform3D());
+
+	return openxr_api->transform_from_pose(entity_poses[p_index]);
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialContextPersistenceConfig
+
+void OpenXRSpatialContextPersistenceConfig::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("add_persistence_context", "persistence_context"), &OpenXRSpatialContextPersistenceConfig::add_persistence_context);
+	ClassDB::bind_method(D_METHOD("remove_persistence_context", "persistence_context"), &OpenXRSpatialContextPersistenceConfig::remove_persistence_context);
+}
+
+bool OpenXRSpatialContextPersistenceConfig::has_valid_configuration() const {
+	OpenXRSpatialAnchorCapability *capability = OpenXRSpatialAnchorCapability::get_singleton();
+	ERR_FAIL_NULL_V(capability, false);
+
+	if (!capability->is_spatial_persistence_supported()) {
+		return false;
+	}
+
+	// Check if we have a valid config.
+	if (persistence_contexts.is_empty()) {
+		return false;
+	}
+
+	return true;
+}
+
+void *OpenXRSpatialContextPersistenceConfig::get_header(void *p_next) {
+	void *n = p_next;
+	if (get_next().is_valid()) {
+		n = get_next()->get_header(n);
+	}
+
+	if (has_valid_configuration()) {
+		OpenXRSpatialAnchorCapability *anchor_capability = OpenXRSpatialAnchorCapability::get_singleton();
+		ERR_FAIL_NULL_V(anchor_capability, nullptr);
+
+		// Prepare our buffer.
+		context_handles.resize(persistence_contexts.size());
+
+		// Copy our handles.
+		XrSpatialPersistenceContextEXT *ptr = context_handles.ptrw();
+		int i = 0;
+		for (const RID &rid : persistence_contexts) {
+			ptr[i++] = anchor_capability->get_persistence_context_handle(rid);
+		}
+
+		persistence_config.next = n;
+		persistence_config.persistenceContextCount = (uint32_t)context_handles.size();
+		persistence_config.persistenceContexts = context_handles.ptr();
+
+		// and return this.
+		return (XrSpatialCapabilityConfigurationBaseHeaderEXT *)&persistence_config;
+	}
+
+	return n;
+}
+
+XrStructureType OpenXRSpatialContextPersistenceConfig::get_structure_type() {
+	return XR_TYPE_SPATIAL_CONTEXT_PERSISTENCE_CONFIG_EXT;
+}
+
+void OpenXRSpatialContextPersistenceConfig::add_persistence_context(RID p_persistence_context) {
+	ERR_FAIL_COND(persistence_contexts.has(p_persistence_context));
+
+	persistence_contexts.push_back(p_persistence_context);
+}
+
+void OpenXRSpatialContextPersistenceConfig::remove_persistence_context(RID p_persistence_context) {
+	ERR_FAIL_COND(!persistence_contexts.has(p_persistence_context));
+
+	persistence_contexts.erase(p_persistence_context);
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialComponentPersistenceList
+
+void OpenXRSpatialComponentPersistenceList::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_persistent_uuid", "index"), &OpenXRSpatialComponentPersistenceList::_get_persistent_uuid);
+	ClassDB::bind_method(D_METHOD("get_persistent_state", "index"), &OpenXRSpatialComponentPersistenceList::_get_persistent_state);
+}
+
+void OpenXRSpatialComponentPersistenceList::set_capacity(uint32_t p_capacity) {
+	persist_data.resize(p_capacity);
+
+	persistence_list.persistDataCount = uint32_t(persist_data.size());
+	persistence_list.persistData = persist_data.ptrw();
+}
+
+XrSpatialComponentTypeEXT OpenXRSpatialComponentPersistenceList::get_component_type() const {
+	return XR_SPATIAL_COMPONENT_TYPE_PERSISTENCE_EXT;
+}
+
+void *OpenXRSpatialComponentPersistenceList::get_structure_data(void *p_next) {
+	persistence_list.next = p_next;
+	return &persistence_list;
+}
+
+XrUuid OpenXRSpatialComponentPersistenceList::get_persistent_uuid(int64_t p_index) const {
+	XrUuid null_uuid = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
+
+	ERR_FAIL_INDEX_V(p_index, persist_data.size(), null_uuid);
+
+	return persist_data[p_index].persistUuid;
+}
+
+String OpenXRSpatialComponentPersistenceList::_get_persistent_uuid(int64_t p_index) const {
+	return OpenXRUtil::string_from_xruuid(get_persistent_uuid(p_index));
+}
+
+XrSpatialPersistenceStateEXT OpenXRSpatialComponentPersistenceList::get_persistent_state(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, persist_data.size(), XR_SPATIAL_PERSISTENCE_STATE_MAX_ENUM_EXT);
+
+	return persist_data[p_index].persistState;
+}
+
+uint64_t OpenXRSpatialComponentPersistenceList::_get_persistent_state(int64_t p_index) const {
+	// TODO make a Godot constant that mirrors XrSpatialPersistenceStateEXT and return that
+	return (uint64_t)get_persistent_state(p_index);
+}
+
+String OpenXRSpatialComponentPersistenceList::get_persistence_state_name(XrSpatialPersistenceStateEXT p_state) {
+	XR_ENUM_SWITCH(XrSpatialPersistenceStateEXT, p_state)
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRAnchorTracker
+
+void OpenXRAnchorTracker::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("has_uuid"), &OpenXRAnchorTracker::has_uuid);
+	ClassDB::bind_method(D_METHOD("set_uuid", "uuid"), &OpenXRAnchorTracker::_set_uuid);
+	ClassDB::bind_method(D_METHOD("get_uuid"), &OpenXRAnchorTracker::_get_uuid);
+
+	ADD_PROPERTY(PropertyInfo(Variant::STRING, "uuid"), "set_uuid", "get_uuid");
+
+	ADD_SIGNAL(MethodInfo("uuid_changed"));
+}
+
+bool OpenXRAnchorTracker::has_uuid() const {
+	for (int i = 0; i < XR_UUID_SIZE; i++) {
+		if (uuid.data[i] != 0) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+XrUuid OpenXRAnchorTracker::get_uuid() const {
+	return uuid;
+}
+
+void OpenXRAnchorTracker::set_uuid(const XrUuid &p_uuid) {
+	if (uuid_is_equal(uuid, p_uuid)) {
+		return;
+	}
+
+	uuid = p_uuid;
+
+	emit_signal(SNAME("uuid_changed"));
+}
+
+String OpenXRAnchorTracker::_get_uuid() const {
+	return OpenXRUtil::string_from_xruuid(uuid);
+}
+
+void OpenXRAnchorTracker::_set_uuid(const String &p_uuid) {
+	set_uuid(OpenXRUtil::xruuid_from_string(p_uuid));
+}
+
+bool OpenXRAnchorTracker::uuid_is_equal(const XrUuid &p_a, const XrUuid &p_b) {
+	for (int i = 0; i < XR_UUID_SIZE; i++) {
+		if (p_a.data[i] != p_b.data[i]) {
+			return false;
+		}
+	}
+
+	return true;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialAnchorCapability
+
+OpenXRSpatialAnchorCapability *OpenXRSpatialAnchorCapability::singleton = nullptr;
+
+OpenXRSpatialAnchorCapability *OpenXRSpatialAnchorCapability::get_singleton() {
+	return singleton;
+}
+
+OpenXRSpatialAnchorCapability::OpenXRSpatialAnchorCapability() {
+	singleton = this;
+}
+
+OpenXRSpatialAnchorCapability::~OpenXRSpatialAnchorCapability() {
+	singleton = nullptr;
+}
+
+void OpenXRSpatialAnchorCapability::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("is_spatial_anchor_supported"), &OpenXRSpatialAnchorCapability::is_spatial_anchor_supported);
+	ClassDB::bind_method(D_METHOD("is_spatial_persistence_supported"), &OpenXRSpatialAnchorCapability::is_spatial_persistence_supported);
+
+	ClassDB::bind_method(D_METHOD("is_persistence_scope_supported", "scope"), &OpenXRSpatialAnchorCapability::_is_persistence_scope_supported);
+	ClassDB::bind_method(D_METHOD("create_persistence_context", "scope", "user_callback"), &OpenXRSpatialAnchorCapability::_create_persistence_context, DEFVAL(Callable()));
+	ClassDB::bind_method(D_METHOD("get_persistence_context_handle", "persistence_context"), &OpenXRSpatialAnchorCapability::_get_persistence_context_handle);
+	ClassDB::bind_method(D_METHOD("free_persistence_context", "persistence_context"), &OpenXRSpatialAnchorCapability::free_persistence_context);
+
+	ClassDB::bind_method(D_METHOD("create_new_anchor", "transform", "spatial_context"), &OpenXRSpatialAnchorCapability::create_new_anchor, DEFVAL(RID()));
+	ClassDB::bind_method(D_METHOD("remove_anchor", "anchor_tracker"), &OpenXRSpatialAnchorCapability::remove_anchor);
+	ClassDB::bind_method(D_METHOD("persist_anchor", "anchor_tracker", "persistence_context", "user_callback"), &OpenXRSpatialAnchorCapability::persist_anchor, DEFVAL(RID()), DEFVAL(Callable()));
+	ClassDB::bind_method(D_METHOD("unpersist_anchor", "anchor_tracker", "persistence_context", "user_callback"), &OpenXRSpatialAnchorCapability::unpersist_anchor, DEFVAL(RID()), DEFVAL(Callable()));
+
+	BIND_ENUM_CONSTANT(PERSISTENCE_SCOPE_SYSTEM_MANAGED);
+	BIND_ENUM_CONSTANT(PERSISTENCE_SCOPE_LOCAL_ANCHORS);
+}
+
+HashMap<String, bool *> OpenXRSpatialAnchorCapability::get_requested_extensions() {
+	HashMap<String, bool *> request_extensions;
+
+	if (GLOBAL_GET_CACHED(bool, "xr/openxr/extensions/spatial_entity/enabled") && GLOBAL_GET_CACHED(bool, "xr/openxr/extensions/spatial_entity/enable_spatial_anchors")) {
+		request_extensions[XR_EXT_SPATIAL_ANCHOR_EXTENSION_NAME] = &spatial_anchor_ext;
+		if (GLOBAL_GET_CACHED(bool, "xr/openxr/extensions/spatial_entity/enable_persistent_anchors")) {
+			request_extensions[XR_EXT_SPATIAL_PERSISTENCE_EXTENSION_NAME] = &spatial_persistence_ext;
+			request_extensions[XR_EXT_SPATIAL_PERSISTENCE_OPERATIONS_EXTENSION_NAME] = &spatial_persistence_operations_ext;
+		}
+	}
+
+	return request_extensions;
+}
+
+void OpenXRSpatialAnchorCapability::on_instance_created(const XrInstance p_instance) {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL(se_extension);
+
+	if (spatial_anchor_ext) {
+		EXT_INIT_XR_FUNC(xrCreateSpatialAnchorEXT);
+	}
+
+	if (spatial_persistence_ext) {
+		OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+		ERR_FAIL_NULL(openxr_api);
+
+		// TODO REMOVE THIS WORKAROUND ONCE POSSIBLE
+		// There has been some back and forth between stores and scopes. Scopes won out.
+		XrResult xr_result = openxr_api->get_instance_proc_addr("xrEnumerateSpatialPersistenceScopesEXT", (PFN_xrVoidFunction *)&xrEnumerateSpatialPersistenceScopesEXT_ptr);
+		if (xr_result != XR_SUCCESS) {
+			// Check for stores for compatibility with beta runtimes.
+			// Lucky for us, related structs and enums are compatible.
+			print_verbose("OpenXR: xrEnumerateSpatialPersistenceScopesEXT is not supported, falling back to xrEnumerateSpatialPersistenceStoresEXT!")
+					xr_result = openxr_api->get_instance_proc_addr("xrEnumerateSpatialPersistenceStoresEXT", (PFN_xrVoidFunction *)&xrEnumerateSpatialPersistenceScopesEXT_ptr);
+		}
+		ERR_FAIL_COND(XR_FAILED(xr_result));
+		//EXT_INIT_XR_FUNC(xrEnumerateSpatialPersistenceScopesEXT);
+
+		EXT_INIT_XR_FUNC(xrCreateSpatialPersistenceContextAsyncEXT);
+		EXT_INIT_XR_FUNC(xrCreateSpatialPersistenceContextCompleteEXT);
+		EXT_INIT_XR_FUNC(xrDestroySpatialPersistenceContextEXT);
+	}
+
+	if (spatial_persistence_operations_ext) {
+		EXT_INIT_XR_FUNC(xrPersistSpatialEntityAsyncEXT);
+		EXT_INIT_XR_FUNC(xrPersistSpatialEntityCompleteEXT);
+		EXT_INIT_XR_FUNC(xrUnpersistSpatialEntityAsyncEXT);
+		EXT_INIT_XR_FUNC(xrUnpersistSpatialEntityCompleteEXT);
+	}
+}
+
+void OpenXRSpatialAnchorCapability::on_instance_destroyed() {
+	xrCreateSpatialAnchorEXT_ptr = nullptr;
+
+	xrEnumerateSpatialPersistenceScopesEXT_ptr = nullptr;
+	xrCreateSpatialPersistenceContextAsyncEXT_ptr = nullptr;
+	xrCreateSpatialPersistenceContextCompleteEXT_ptr = nullptr;
+	xrDestroySpatialPersistenceContextEXT_ptr = nullptr;
+}
+
+void OpenXRSpatialAnchorCapability::on_session_created(const XrSession p_session) {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL(se_extension);
+
+	if (!spatial_anchor_ext) {
+		return;
+	}
+
+	spatial_anchor_supported = se_extension->supports_component_type(XR_SPATIAL_CAPABILITY_ANCHOR_EXT, XR_SPATIAL_COMPONENT_TYPE_ANCHOR_EXT);
+	if (!spatial_anchor_supported) {
+		// Supported by XR runtime but not by device? We're done.
+		return;
+	}
+
+	_load_supported_persistence_scopes();
+
+	se_extension->connect(SNAME("spatial_discovery_recommended"), callable_mp(this, &OpenXRSpatialAnchorCapability::_on_spatial_discovery_recommended));
+
+	if (GLOBAL_GET_CACHED(bool, "xr/openxr/extensions/spatial_entity/enable_builtin_anchor_detection")) {
+		if (spatial_persistence_ext && !supported_persistence_scopes.is_empty()) {
+			// TODO make something nicer to select the persistence scope we want.
+			// We may even want to create multiple here so we get access to all
+			// but then mark one that we use to create our persistent anchors on.
+
+			XrSpatialPersistenceScopeEXT scope = XR_SPATIAL_PERSISTENCE_SCOPE_MAX_ENUM_EXT;
+
+			// Lets check these in order of importance to us and find the best applicable scope.
+			if (supported_persistence_scopes.has(XR_SPATIAL_PERSISTENCE_SCOPE_LOCAL_ANCHORS_EXT)) {
+				// This scope allows for local storage and is required if we want to create our own anchors.
+				scope = XR_SPATIAL_PERSISTENCE_SCOPE_LOCAL_ANCHORS_EXT;
+			} else if (supported_persistence_scopes.has(XR_SPATIAL_PERSISTENCE_SCOPE_SYSTEM_MANAGED_EXT)) {
+				// The system managed scope is a read only scope with system managed anchors.
+				scope = XR_SPATIAL_PERSISTENCE_SCOPE_SYSTEM_MANAGED_EXT;
+			} else {
+				// Just use the first supported scope, but this will be an unknown type.
+				scope = supported_persistence_scopes[0];
+			}
+
+			// Output what we're using:
+			print_verbose("OpenXR: Using persistence scope " + get_spatial_persistence_scope_name(scope));
+
+			// Start by creating our persistence context.
+			create_persistence_context(scope, callable_mp(this, &OpenXRSpatialAnchorCapability::_on_persistence_context_completed));
+		} else {
+			// Start by creating our spatial context
+			_create_spatial_context();
+		}
+	}
+}
+
+void OpenXRSpatialAnchorCapability::on_session_destroyed() {
+	if (!spatial_anchor_supported) {
+		return;
+	}
+	spatial_anchor_supported = false;
+
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL(se_extension);
+	XRServer *xr_server = XRServer::get_singleton();
+	ERR_FAIL_NULL(xr_server);
+
+	// Free and unregister our anchors
+	for (const KeyValue<XrSpatialEntityIdEXT, Ref<OpenXRAnchorTracker>> &anchor : anchors) {
+		xr_server->remove_tracker(anchor.value);
+	}
+	anchors.clear();
+
+	// Free our configurations
+	anchor_configuration.unref();
+
+	// Free our spatial context
+	if (spatial_context.is_valid()) {
+		se_extension->free_spatial_context(spatial_context);
+		spatial_context = RID();
+	}
+
+	// Free our persistence context
+	if (persistence_context.is_valid()) {
+		free_persistence_context(persistence_context);
+		persistence_context = RID();
+	}
+
+	se_extension->disconnect(SNAME("spatial_discovery_recommended"), callable_mp(this, &OpenXRSpatialAnchorCapability::_on_spatial_discovery_recommended));
+
+	supported_persistence_scopes.clear();
+
+	// Clean up all remaining persistence context RIDs.
+	LocalVector<RID> persistence_context_rids = persistence_context_owner.get_owned_list();
+	for (const RID &rid : persistence_context_rids) {
+		if (is_print_verbose_enabled()) {
+			PersistenceContextData *persistence_context_data = persistence_context_owner.get_or_null(rid);
+			if (persistence_context_data) { // Should never be nullptr seeing we called get_owned_list just now, but just in case.
+				print_line("OpenXR: Found orphaned persistence context for scope ", get_spatial_persistence_scope_name(persistence_context_data->scope));
+			}
+		}
+
+		free_persistence_context(rid);
+	}
+}
+
+void OpenXRSpatialAnchorCapability::on_process() {
+	if (!spatial_context.is_valid()) {
+		return;
+	}
+
+	// Protection against plane discovery happening too often.
+	if (discovery_cooldown > 0) {
+		discovery_cooldown--;
+	}
+
+	// Check if we need to start our discovery.
+	if (need_discovery && discovery_cooldown == 0 && !discovery_query_result.is_valid()) {
+		need_discovery = false;
+		discovery_cooldown = 60; // Set our cooldown to 60 frames, it doesn't need to be an exact science.
+
+		_start_entity_discovery();
+	}
+
+	// If we have a valid spatial context, and we have anchors, we want updates!
+	if (spatial_context.is_valid() && !anchors.is_empty()) {
+		OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+		ERR_FAIL_NULL(se_extension);
+
+		// We want updates for all anchors
+		thread_local LocalVector<RID> entities;
+		entities.resize(anchors.size());
+		RID *entity = entities.ptr();
+		for (const KeyValue<XrSpatialEntityIdEXT, Ref<OpenXRAnchorTracker>> &e : anchors) {
+			*entity = e.value->get_entity();
+			entity++;
+		}
+
+		// We just want our anchor component
+		thread_local LocalVector<XrSpatialComponentTypeEXT> component_types;
+		component_types.push_back(XR_SPATIAL_COMPONENT_TYPE_ANCHOR_EXT);
+
+		// And we get our update snapshot, this is NOT async!
+		RID snapshot = se_extension->update_spatial_entities(spatial_context, entities, component_types, nullptr);
+		if (snapshot.is_valid()) {
+			_process_update_snapshot(snapshot);
+		}
+	}
+}
+
+bool OpenXRSpatialAnchorCapability::is_spatial_anchor_supported() {
+	return spatial_anchor_supported;
+}
+
+bool OpenXRSpatialAnchorCapability::is_spatial_persistence_supported() {
+	// Need anchor support for persistence to be usable
+	if (!is_spatial_anchor_supported()) {
+		return false;
+	}
+
+	return spatial_persistence_ext;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Persistence scopes
+
+bool OpenXRSpatialAnchorCapability::_load_supported_persistence_scopes() {
+	ERR_FAIL_COND_V(!is_spatial_persistence_supported(), false);
+
+	if (supported_persistence_scopes.is_empty()) {
+		OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+		ERR_FAIL_NULL_V(openxr_api, false);
+
+		uint32_t size;
+		XrInstance instance = openxr_api->get_instance();
+		XrSystemId system_id = openxr_api->get_system_id();
+
+		ERR_FAIL_COND_V(instance == XR_NULL_HANDLE, false);
+
+		XrResult result = xrEnumerateSpatialPersistenceScopesEXT(instance, system_id, 0, &size, nullptr);
+		if (XR_FAILED(result)) {
+			ERR_FAIL_V_MSG(false, "OpenXR: Failed to query persistence scope count [" + openxr_api->get_error_string(result) + "]");
+		}
+
+		if (size > 0) {
+			supported_persistence_scopes.resize(size);
+			result = xrEnumerateSpatialPersistenceScopesEXT(instance, system_id, supported_persistence_scopes.size(), &size, supported_persistence_scopes.ptrw());
+			if (XR_FAILED(result)) {
+				ERR_FAIL_V_MSG(false, "OpenXR: Failed to query persistence scopes [" + openxr_api->get_error_string(result) + "]");
+			}
+		}
+
+		if (is_print_verbose_enabled()) {
+			if (!supported_persistence_scopes.is_empty()) {
+				print_verbose("OpenXR: Supported spatial persistence scopes:");
+				for (const XrSpatialPersistenceScopeEXT &scope : supported_persistence_scopes) {
+					print_verbose(" - " + get_spatial_persistence_scope_name(scope));
+				}
+			} else {
+				WARN_PRINT("OpenXR: No persistence scopes found!");
+			}
+		}
+	}
+
+	return true;
+}
+
+bool OpenXRSpatialAnchorCapability::is_persistence_scope_supported(XrSpatialPersistenceScopeEXT p_scope) {
+	if (!is_spatial_persistence_supported()) {
+		return false;
+	}
+
+	if (!_load_supported_persistence_scopes()) {
+		return false;
+	}
+
+	return supported_persistence_scopes.has(p_scope);
+}
+
+bool OpenXRSpatialAnchorCapability::_is_persistence_scope_supported(PersistenceScope p_scope) {
+	return is_persistence_scope_supported((XrSpatialPersistenceScopeEXT)p_scope);
+}
+
+Ref<OpenXRFutureResult> OpenXRSpatialAnchorCapability::create_persistence_context(XrSpatialPersistenceScopeEXT p_scope, const Callable &p_user_callback) {
+	if (!is_spatial_persistence_supported()) {
+		return nullptr;
+	}
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, nullptr);
+
+	OpenXRFutureExtension *future_api = OpenXRFutureExtension::get_singleton();
+	ERR_FAIL_NULL_V(future_api, nullptr);
+
+	XrSpatialPersistenceContextCreateInfoEXT create_info = {
+		XR_TYPE_SPATIAL_PERSISTENCE_CONTEXT_CREATE_INFO_EXT, // type
+		nullptr, // next
+		p_scope // scope
+	};
+	XrFutureEXT future = XR_NULL_HANDLE;
+	XrResult result = xrCreateSpatialPersistenceContextAsyncEXT(openxr_api->get_session(), &create_info, &future);
+	if (XR_FAILED(result)) {
+		// Not successful? then exit.
+		ERR_FAIL_V_MSG(Ref<OpenXRFutureResult>(), "OpenXR: Failed to create persistence scope [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	// Create our future result
+	Ref<OpenXRFutureResult> future_result = future_api->register_future(future, callable_mp(this, &OpenXRSpatialAnchorCapability::_on_persistence_context_ready).bind((uint64_t)p_scope, p_user_callback));
+
+	return future_result;
+}
+
+Ref<OpenXRFutureResult> OpenXRSpatialAnchorCapability::_create_persistence_context(PersistenceScope p_scope, Callable p_user_callback) {
+	return create_persistence_context((XrSpatialPersistenceScopeEXT)p_scope, p_user_callback);
+}
+
+void OpenXRSpatialAnchorCapability::_on_persistence_context_ready(Ref<OpenXRFutureResult> p_future_result, uint64_t p_scope, Callable p_user_callback) {
+	// Complete context creation...
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+
+	XrCreateSpatialPersistenceContextCompletionEXT completion = {
+		XR_TYPE_CREATE_SPATIAL_PERSISTENCE_CONTEXT_COMPLETION_EXT, // type
+		nullptr, // next
+		XR_RESULT_MAX_ENUM, // futureResult
+		XR_SPATIAL_PERSISTENCE_CONTEXT_RESULT_MAX_ENUM_EXT, // createResult
+		XR_NULL_HANDLE // persistenceContext
+	};
+	XrResult result = xrCreateSpatialPersistenceContextCompleteEXT(openxr_api->get_session(), p_future_result->get_future(), &completion);
+	if (XR_FAILED(result)) { // Did our xrCreateSpatialContextCompleteEXT call fail?
+		// Log issue and fail.
+		ERR_FAIL_MSG("OpenXR: Failed to complete persistence context create future [" + openxr_api->get_error_string(result) + "]");
+	}
+	if (XR_FAILED(completion.futureResult)) { // Did our completion fail?
+		// Log issue and fail.
+		ERR_FAIL_MSG("OpenXR: Failed to complete persistence context creation [" + openxr_api->get_error_string(completion.futureResult) + "]");
+	}
+	if (completion.createResult != XR_SPATIAL_PERSISTENCE_CONTEXT_RESULT_SUCCESS_EXT) { // Did our persist fail?
+		// Log issue and fail.
+		ERR_FAIL_MSG("OpenXR: Failed to complete persistence context creation [" + get_spatial_persistence_context_result_name(completion.createResult) + "]");
+	}
+
+	// Wrap our persistence context
+	PersistenceContextData persistence_context_data;
+
+	// Update our spatial context data
+	persistence_context_data.scope = (XrSpatialPersistenceScopeEXT)p_scope;
+	persistence_context_data.persistence_context = completion.persistenceContext;
+
+	// Store this as an RID so we keep track of it.
+	RID context_rid = persistence_context_owner.make_rid(persistence_context_data);
+
+	// Set our RID as our result value on our future.
+	p_future_result->set_result_value(context_rid);
+
+	// And perform our callback if we have one.
+	if (p_user_callback.is_valid()) {
+		p_user_callback.call(context_rid);
+	}
+}
+
+XrSpatialPersistenceContextEXT OpenXRSpatialAnchorCapability::get_persistence_context_handle(RID p_persistence_context) const {
+	PersistenceContextData *persistence_context_data = persistence_context_owner.get_or_null(p_persistence_context);
+	ERR_FAIL_NULL_V(persistence_context_data, XR_NULL_HANDLE);
+
+	return persistence_context_data->persistence_context;
+}
+
+uint64_t OpenXRSpatialAnchorCapability::_get_persistence_context_handle(RID p_persistence_context) const {
+	return (uint64_t)get_persistence_context_handle(p_persistence_context);
+}
+
+void OpenXRSpatialAnchorCapability::free_persistence_context(RID p_persistence_context) {
+	PersistenceContextData *persistence_context_data = persistence_context_owner.get_or_null(p_persistence_context);
+	ERR_FAIL_NULL(persistence_context_data);
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+
+	if (persistence_context_data->persistence_context != XR_NULL_HANDLE) {
+		// Destroy our spatial context
+		XrResult result = xrDestroySpatialPersistenceContextEXT(persistence_context_data->persistence_context);
+		if (XR_FAILED(result)) {
+			WARN_PRINT("OpenXR: Failed to destroy the persistence context [" + openxr_api->get_error_string(result) + "]");
+		}
+		persistence_context_data->persistence_context = XR_NULL_HANDLE;
+	}
+
+	// And remove our RID.
+	persistence_context_owner.free(p_persistence_context);
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Discovery logic
+
+void OpenXRSpatialAnchorCapability::_on_persistence_context_completed(RID p_persistence_context) {
+	persistence_context = p_persistence_context;
+
+	_create_spatial_context();
+}
+
+Ref<OpenXRFutureResult> OpenXRSpatialAnchorCapability::_create_spatial_context() {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, nullptr);
+
+	TypedArray<OpenXRSpatialCapabilityConfigurationBaseHeader> capability_configurations;
+
+	// Create our configuration objects.
+	anchor_configuration.instantiate();
+	capability_configurations.push_back(anchor_configuration);
+
+	if (persistence_context.is_valid()) {
+		persistence_configuration.instantiate();
+		persistence_configuration->add_persistence_context(persistence_context);
+	} else {
+		// Shouldn't be instantiated in the first place but JIC
+		persistence_configuration.unref();
+	}
+
+	return se_extension->create_spatial_context(capability_configurations, persistence_configuration, callable_mp(this, &OpenXRSpatialAnchorCapability::_on_spatial_context_created));
+}
+
+void OpenXRSpatialAnchorCapability::_on_spatial_context_created(RID p_spatial_context) {
+	spatial_context = p_spatial_context;
+	need_discovery = true;
+}
+
+void OpenXRSpatialAnchorCapability::_on_spatial_discovery_recommended(RID p_spatial_context) {
+	if (p_spatial_context == spatial_context) {
+		// Trigger new discovery.
+		need_discovery = true;
+	}
+}
+
+Ref<OpenXRFutureResult> OpenXRSpatialAnchorCapability::_start_entity_discovery() {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, nullptr);
+
+	// It makes no sense to discover non persistent anchors as we'd have created them during this session.
+	if (!persistence_context.is_valid()) {
+		return nullptr;
+	}
+
+	// Already running or ran discovery, cancel/clean up.
+	if (discovery_query_result.is_valid()) {
+		discovery_query_result->cancel_future();
+		discovery_query_result.unref();
+	}
+
+	// We want both our anchor and persistence component.
+	Vector<XrSpatialComponentTypeEXT> component_types;
+	component_types.push_back(XR_SPATIAL_COMPONENT_TYPE_ANCHOR_EXT);
+	component_types.push_back(XR_SPATIAL_COMPONENT_TYPE_PERSISTENCE_EXT);
+
+	// Start our new snapshot.
+	discovery_query_result = se_extension->discover_spatial_entities(spatial_context, component_types, nullptr, callable_mp(this, &OpenXRSpatialAnchorCapability::_process_discovery_snapshot));
+
+	return discovery_query_result;
+}
+
+void OpenXRSpatialAnchorCapability::_process_discovery_snapshot(RID p_snapshot) {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL(se_extension);
+	XRServer *xr_server = XRServer::get_singleton();
+	ERR_FAIL_NULL(xr_server);
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+
+	// Build our component data.
+	TypedArray<OpenXRSpatialComponentData> component_data;
+
+	// We always need a query result data object.
+	Ref<OpenXRSpatialQueryResultData> query_result_data;
+	query_result_data.instantiate();
+	component_data.push_back(query_result_data);
+
+	// And an anchor list object.
+	Ref<OpenXRSpatialComponentAnchorList> anchor_list_data;
+	anchor_list_data.instantiate();
+	component_data.push_back(anchor_list_data);
+
+	// Note that adding this data object means our snapshot will only return persistent anchors!
+	Ref<OpenXRSpatialComponentPersistenceList> persistence_list_data;
+	persistence_list_data.instantiate();
+	component_data.push_back(persistence_list_data);
+
+	if (se_extension->query_snapshot(p_snapshot, component_data, nullptr)) {
+		// Now loop through our data and update our anchors.
+		int64_t size = query_result_data->get_capacity();
+		for (int64_t i = 0; i < size; i++) {
+			XrSpatialEntityIdEXT entity_id = query_result_data->get_entity_id(i);
+			XrSpatialEntityTrackingStateEXT entity_state = query_result_data->get_entity_state(i);
+
+			if (entity_state == XR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED_EXT) {
+				// We shouldn't get stopped anchors for discovery queries, but JIC.
+				if (anchors.has(entity_id)) {
+					Ref<OpenXRAnchorTracker> anchor = anchors[entity_id];
+					anchor->invalidate_pose(SNAME("default"));
+					anchor->set_spatial_tracking_state(XR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED_EXT);
+				}
+			} else {
+				// Process our entity.
+				bool add_to_xr_server = false;
+				Ref<OpenXRAnchorTracker> anchor;
+
+				if (anchors.has(entity_id)) {
+					// We know about this one already.
+					anchor = anchors[entity_id];
+				} else {
+					// Create a new anchor.
+					anchor.instantiate();
+					anchor->set_entity(se_extension->make_spatial_entity(se_extension->get_spatial_snapshot_context(p_snapshot), entity_id));
+					anchors[entity_id] = anchor;
+
+					add_to_xr_server = true;
+				}
+
+				// Handle component data.
+				if (entity_state == XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT) {
+					anchor->invalidate_pose(SNAME("default"));
+					anchor->set_spatial_tracking_state(XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT);
+
+					// No further component data will be valid in this state, we need to ignore it!
+				} else if (entity_state == XR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING_EXT) {
+					Transform3D transform = anchor_list_data->get_entity_pose(i);
+					anchor->set_pose(SNAME("default"), transform, Vector3(), Vector3());
+					anchor->set_spatial_tracking_state(XR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING_EXT);
+				}
+
+				// Persistence is the only component that will contain valid data if entity_state == XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT.
+				const XrSpatialPersistenceStateEXT persistent_state = persistence_list_data->get_persistent_state(i);
+				if (persistent_state == XR_SPATIAL_PERSISTENCE_STATE_LOADED_EXT) {
+					anchor->set_uuid(persistence_list_data->get_persistent_uuid(i));
+				}
+
+				if (add_to_xr_server) {
+					// Register with XR server.
+					xr_server->add_tracker(anchor);
+				}
+			}
+		}
+
+		// We don't remove trackers here, users will be removing anchors.
+		// Maybe at some point when shared anchors between headsets result
+		// in another device removing the shared anchor we need to deal with this.
+	}
+
+	// Now that we're done, clean up our snapshot!
+	se_extension->free_spatial_snapshot(p_snapshot);
+
+	// And if this was our discovery snapshot, let's reset it.
+	if (discovery_query_result.is_valid() && discovery_query_result->get_result_value() == p_snapshot) {
+		discovery_query_result.unref();
+	}
+}
+
+void OpenXRSpatialAnchorCapability::_process_update_snapshot(RID p_snapshot) {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL(se_extension);
+	XRServer *xr_server = XRServer::get_singleton();
+	ERR_FAIL_NULL(xr_server);
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+
+	// Build our component data.
+	TypedArray<OpenXRSpatialComponentData> component_data;
+
+	// We always need a query result data object.
+	Ref<OpenXRSpatialQueryResultData> query_result_data;
+	query_result_data.instantiate();
+	component_data.push_back(query_result_data);
+
+	// And an anchor list object.
+	Ref<OpenXRSpatialComponentAnchorList> anchor_list_data;
+	anchor_list_data.instantiate();
+	component_data.push_back(anchor_list_data);
+
+	if (se_extension->query_snapshot(p_snapshot, component_data, nullptr)) {
+		// Now loop through our data and update our anchors.
+		int64_t size = query_result_data->get_capacity();
+		for (int64_t i = 0; i < size; i++) {
+			XrSpatialEntityIdEXT entity_id = query_result_data->get_entity_id(i);
+			XrSpatialEntityTrackingStateEXT entity_state = query_result_data->get_entity_state(i);
+
+			if (anchors.has(entity_id)) {
+				// Process our entity.
+				Ref<OpenXRAnchorTracker> anchor = anchors[entity_id];
+
+				if (entity_state == XR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED_EXT) {
+					anchor->invalidate_pose(SNAME("default"));
+					anchor->set_spatial_tracking_state(XR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED_EXT);
+				} else if (entity_state == XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT) {
+					anchor->invalidate_pose(SNAME("default"));
+					anchor->set_spatial_tracking_state(XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT);
+				} else if (entity_state == XR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING_EXT) {
+					Transform3D transform = anchor_list_data->get_entity_pose(i);
+					anchor->set_pose(SNAME("default"), transform, Vector3(), Vector3());
+					anchor->set_spatial_tracking_state(XR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING_EXT);
+				}
+			} else {
+				WARN_PRINT("OpenXR: Anchor update query returned unknown anchor with entity ID: " + String::num_int64(entity_id));
+			}
+		}
+	}
+
+	// Now that we're done, clean up our snapshot!
+	se_extension->free_spatial_snapshot(p_snapshot);
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Anchor creation
+
+Ref<OpenXRAnchorTracker> OpenXRSpatialAnchorCapability::create_new_anchor(const Transform3D &p_transform, RID p_spatial_context) {
+	Ref<OpenXRAnchorTracker> tracker;
+
+	ERR_FAIL_COND_V_MSG(!is_spatial_anchor_supported(), tracker, "OpenXR: Spatial entity anchor capability is not supported on this hardware!");
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, tracker);
+	XRServer *xr_server = XRServer::get_singleton();
+	ERR_FAIL_NULL_V(xr_server, tracker);
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, tracker);
+
+	// TODO reverse apply world scale and reference frame to transform.
+
+	XrPosef pose = openxr_api->pose_from_transform(p_transform);
+
+	RID sc = p_spatial_context.is_valid() ? p_spatial_context : spatial_context;
+	ERR_FAIL_COND_V(sc.is_null(), tracker);
+
+	XrSpatialAnchorCreateInfoEXT create_info = {
+		XR_TYPE_SPATIAL_ANCHOR_CREATE_INFO_EXT, // type
+		nullptr, // next
+		openxr_api->get_play_space(), // baseSpace
+		openxr_api->get_predicted_display_time(), // time
+		pose // pose
+	};
+	XrSpatialEntityIdEXT entity_id;
+	XrSpatialEntityEXT entity;
+	XrResult result = xrCreateSpatialAnchorEXT(se_extension->get_spatial_context_handle(sc), &create_info, &entity_id, &entity);
+	if (XR_FAILED(result)) { // Did our xrCreateSpatialContextCompleteEXT call fail?
+		ERR_FAIL_V_MSG(tracker, "OpenXR: Failed to create anchor [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	tracker.instantiate();
+	tracker->set_entity(se_extension->add_spatial_entity(sc, entity_id, entity));
+	tracker->set_tracker_desc("Anchor");
+	tracker->set_pose(SNAME("default"), p_transform, Vector3(), Vector3());
+
+	// Remember our tracker.
+	anchors[entity_id] = tracker;
+	xr_server->add_tracker(tracker);
+
+	return tracker;
+}
+
+void OpenXRSpatialAnchorCapability::remove_anchor(Ref<OpenXRAnchorTracker> p_anchor_tracker) {
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+	XRServer *xr_server = XRServer::get_singleton();
+	ERR_FAIL_NULL(xr_server);
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL(se_extension);
+
+	// We check for this here. We could do this asynchronous but the caller may than wrongly expect this method to be instant.
+	ERR_FAIL_COND_MSG(p_anchor_tracker->has_uuid(), "OpenXR: This anchor is persistent. It must first be made unpersistent.");
+
+	// Attempt to unregister it from our xr_server.
+	xr_server->remove_tracker(p_anchor_tracker);
+
+	// Get our entity.
+	RID entity = p_anchor_tracker->get_entity();
+	ERR_FAIL_COND(entity.is_null());
+
+	// Get our entity id.
+	XrSpatialEntityIdEXT entity_id = se_extension->get_spatial_entity_id(entity);
+	ERR_FAIL_COND(entity_id == XR_NULL_ENTITY);
+
+	// Remove it from our entity list.
+	if (anchors.has(entity_id)) {
+		anchors.erase(entity_id);
+	}
+
+	// Clear our entity, this will free it as well.
+	p_anchor_tracker->set_entity(RID());
+
+	// The anchor tracker will be cleaned up once its fully dereferenced.
+}
+
+Ref<OpenXRFutureResult> OpenXRSpatialAnchorCapability::persist_anchor(Ref<OpenXRAnchorTracker> p_anchor_tracker, RID p_persistence_context, const Callable &p_user_callback) {
+	ERR_FAIL_COND_V(!is_spatial_persistence_supported(), nullptr);
+
+	RID pc = p_persistence_context.is_valid() ? p_persistence_context : persistence_context;
+	ERR_FAIL_COND_V(pc.is_null(), nullptr);
+	ERR_FAIL_COND_V(p_anchor_tracker.is_null(), nullptr);
+	PersistenceContextData *persistence_context_data = persistence_context_owner.get_or_null(pc);
+	ERR_FAIL_NULL_V(persistence_context_data, nullptr);
+	const XrSpatialPersistenceContextEXT persistence_context_handle = persistence_context_data->persistence_context;
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, nullptr);
+	OpenXRFutureExtension *future_api = OpenXRFutureExtension::get_singleton();
+	ERR_FAIL_NULL_V(future_api, nullptr);
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, nullptr);
+
+	RID entity = p_anchor_tracker->get_entity();
+	ERR_FAIL_COND_V(entity.is_null(), nullptr);
+
+	XrSpatialEntityIdEXT entity_id = se_extension->get_spatial_entity_id(entity);
+	ERR_FAIL_COND_V(entity_id == XR_NULL_ENTITY, nullptr);
+
+	RID spatial_context_rid = se_extension->get_spatial_entity_context(entity);
+	const XrSpatialContextEXT spatial_context_handle = se_extension->get_spatial_context_handle(spatial_context_rid);
+
+	XrFutureEXT future = XR_NULL_HANDLE;
+
+	XrSpatialEntityPersistInfoEXT persist_info = {
+		XR_TYPE_SPATIAL_ENTITY_PERSIST_INFO_EXT, // type
+		nullptr, // next
+		spatial_context_handle, // spatialContext
+		entity_id // entityId
+	};
+	XrResult result = xrPersistSpatialEntityAsyncEXT(persistence_context_handle, &persist_info, &future);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(nullptr, "OpenXR: Failed to start making anchor persistent [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	// Create our future result.
+	Ref<OpenXRFutureResult> future_result = future_api->register_future(future, callable_mp(this, &OpenXRSpatialAnchorCapability::_on_made_anchor_persistent).bind(pc, p_anchor_tracker, p_user_callback));
+
+	return future_result;
+}
+
+void OpenXRSpatialAnchorCapability::_on_made_anchor_persistent(Ref<OpenXRFutureResult> p_future_result, RID p_persistence_context, Ref<OpenXRAnchorTracker> p_anchor_tracker, const Callable &p_user_callback) {
+	ERR_FAIL_COND(p_anchor_tracker.is_null());
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+	PersistenceContextData *persistence_context_data = persistence_context_owner.get_or_null(p_persistence_context);
+	ERR_FAIL_NULL(persistence_context_data);
+
+	XrFutureEXT future = p_future_result->get_future();
+
+	XrPersistSpatialEntityCompletionEXT completion = {
+		XR_TYPE_PERSIST_SPATIAL_ENTITY_COMPLETION_EXT, // type
+		nullptr, // next
+		XR_RESULT_MAX_ENUM, // futureResult
+		XR_SPATIAL_PERSISTENCE_CONTEXT_RESULT_MAX_ENUM_EXT, // persistResult
+		{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } // persistUuid
+	};
+	XrResult result = xrPersistSpatialEntityCompleteEXT(persistence_context_data->persistence_context, future, &completion);
+	if (XR_FAILED(result)) { // Did our xrCreateSpatialContextCompleteEXT call fail?
+		p_future_result->set_result_value(false);
+		ERR_FAIL_MSG("OpenXR: Failed to complete anchor persistent future [" + openxr_api->get_error_string(result) + "]");
+	}
+	if (XR_FAILED(completion.futureResult)) { // Did our completion fail?
+		p_future_result->set_result_value(false);
+		ERR_FAIL_MSG("OpenXR: Failed to complete making anchor persistent [" + openxr_api->get_error_string(completion.futureResult) + "]");
+	}
+	if (completion.persistResult != XR_SPATIAL_PERSISTENCE_CONTEXT_RESULT_SUCCESS_EXT) { // Did our process fail?
+		p_future_result->set_result_value(false);
+		ERR_FAIL_MSG("OpenXR: Failed to make anchor persistent [" + get_spatial_persistence_context_result_name(completion.persistResult) + "]");
+	}
+
+	// Set our new UUID.
+	p_anchor_tracker->set_uuid(completion.persistUuid);
+
+	// Set true our result value on our future.
+	p_future_result->set_result_value(true);
+
+	// Do our callback.
+	p_user_callback.call(p_anchor_tracker);
+}
+
+Ref<OpenXRFutureResult> OpenXRSpatialAnchorCapability::unpersist_anchor(Ref<OpenXRAnchorTracker> p_anchor_tracker, RID p_persistence_context, const Callable &p_user_callback) {
+	ERR_FAIL_COND_V(!is_spatial_persistence_supported(), nullptr);
+
+	RID pc = p_persistence_context.is_valid() ? p_persistence_context : persistence_context;
+	ERR_FAIL_COND_V(pc.is_null(), nullptr);
+	ERR_FAIL_COND_V(p_anchor_tracker.is_null(), nullptr);
+	PersistenceContextData *persistence_context_data = persistence_context_owner.get_or_null(pc);
+	ERR_FAIL_NULL_V(persistence_context_data, nullptr);
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, nullptr);
+	OpenXRFutureExtension *future_api = OpenXRFutureExtension::get_singleton();
+	ERR_FAIL_NULL_V(future_api, nullptr);
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, nullptr);
+
+	XrFutureEXT future;
+
+	XrSpatialEntityUnpersistInfoEXT unpersist_info = {
+		XR_TYPE_SPATIAL_ENTITY_UNPERSIST_INFO_EXT, // type
+		nullptr, // next
+		p_anchor_tracker->get_uuid() // persistUuid
+	};
+	XrResult result = xrUnpersistSpatialEntityAsyncEXT(persistence_context_data->persistence_context, &unpersist_info, &future);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(nullptr, "OpenXR: Failed to make anchor unpersistent [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	// Create our future result.
+	Ref<OpenXRFutureResult> future_result = future_api->register_future(future, callable_mp(this, &OpenXRSpatialAnchorCapability::_on_made_anchor_unpersistent).bind(pc, p_anchor_tracker, p_user_callback));
+
+	return future_result;
+}
+
+void OpenXRSpatialAnchorCapability::_on_made_anchor_unpersistent(Ref<OpenXRFutureResult> p_future_result, RID p_persistence_context, Ref<OpenXRAnchorTracker> p_anchor_tracker, const Callable &p_user_callback) {
+	ERR_FAIL_COND(p_anchor_tracker.is_null());
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+	PersistenceContextData *persistence_context_data = persistence_context_owner.get_or_null(p_persistence_context);
+	ERR_FAIL_NULL(persistence_context_data);
+
+	XrFutureEXT future = p_future_result->get_future();
+
+	XrUnpersistSpatialEntityCompletionEXT completion = {
+		XR_TYPE_UNPERSIST_SPATIAL_ENTITY_COMPLETION_EXT, // type
+		nullptr, // next
+		XR_RESULT_MAX_ENUM, // futureResult
+		XR_SPATIAL_PERSISTENCE_CONTEXT_RESULT_MAX_ENUM_EXT, // unpersistResult
+	};
+	XrResult result = xrUnpersistSpatialEntityCompleteEXT(persistence_context_data->persistence_context, future, &completion);
+	if (XR_FAILED(result)) { // Did our xrCreateSpatialContextCompleteEXT call fail?
+		p_future_result->set_result_value(false);
+		ERR_FAIL_MSG("OpenXR: Failed to complete anchor unpersistent future [" + openxr_api->get_error_string(result) + "]");
+	}
+	if (XR_FAILED(completion.futureResult)) { // Did our completion fail?
+		p_future_result->set_result_value(false);
+		ERR_FAIL_MSG("OpenXR: Failed to complete making anchor unpersistent [" + openxr_api->get_error_string(completion.futureResult) + "]");
+	}
+	if (completion.unpersistResult != XR_SPATIAL_PERSISTENCE_CONTEXT_RESULT_SUCCESS_EXT) { // Did our process fail?
+		p_future_result->set_result_value(false);
+		ERR_FAIL_MSG("OpenXR: Failed to make anchor unpersistent [" + get_spatial_persistence_context_result_name(completion.unpersistResult) + "]");
+	}
+
+	// Unset our UUID.
+	XrUuid empty_uid = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
+	p_anchor_tracker->set_uuid(empty_uid);
+
+	// Set true our result value on our future.
+	p_future_result->set_result_value(true);
+
+	// Do our callback.
+	p_user_callback.call(p_anchor_tracker);
+}
+
+String OpenXRSpatialAnchorCapability::get_spatial_persistence_scope_name(XrSpatialPersistenceScopeEXT p_scope){
+	XR_ENUM_SWITCH(XrSpatialPersistenceScopeEXT, p_scope)
+}
+
+String OpenXRSpatialAnchorCapability::get_spatial_persistence_context_result_name(XrSpatialPersistenceContextResultEXT p_result) {
+	XR_ENUM_SWITCH(XrSpatialPersistenceContextResultEXT, p_result)
+}

+ 256 - 0
modules/openxr/extensions/spatial_entities/openxr_spatial_anchor.h

@@ -0,0 +1,256 @@
+/**************************************************************************/
+/*  openxr_spatial_anchor.h                                               */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "../../openxr_util.h"
+#include "openxr_spatial_entities.h"
+#include "openxr_spatial_entity_extension.h"
+
+// Anchor capability configuration
+class OpenXRSpatialCapabilityConfigurationAnchor : public OpenXRSpatialCapabilityConfigurationBaseHeader {
+	GDCLASS(OpenXRSpatialCapabilityConfigurationAnchor, OpenXRSpatialCapabilityConfigurationBaseHeader);
+
+public:
+	virtual bool has_valid_configuration() const override;
+	virtual XrSpatialCapabilityConfigurationBaseHeaderEXT *get_configuration() override;
+
+	Vector<XrSpatialComponentTypeEXT> get_enabled_components() const { return anchor_enabled_components; }
+
+protected:
+	static void _bind_methods();
+
+private:
+	Vector<XrSpatialComponentTypeEXT> anchor_enabled_components;
+	XrSpatialCapabilityConfigurationAnchorEXT anchor_config = { XR_TYPE_SPATIAL_CAPABILITY_CONFIGURATION_ANCHOR_EXT, nullptr, XR_SPATIAL_CAPABILITY_ANCHOR_EXT, 0, nullptr };
+
+	PackedInt64Array _get_enabled_components() const;
+};
+
+// Anchor component anchor list
+class OpenXRSpatialComponentAnchorList : public OpenXRSpatialComponentData {
+	GDCLASS(OpenXRSpatialComponentAnchorList, OpenXRSpatialComponentData);
+
+protected:
+	static void _bind_methods();
+
+public:
+	virtual void set_capacity(uint32_t p_capacity) override;
+	virtual XrSpatialComponentTypeEXT get_component_type() const override;
+	virtual void *get_structure_data(void *p_next) override;
+
+	Transform3D get_entity_pose(int64_t p_index) const;
+
+private:
+	Vector<XrPosef> entity_poses;
+
+	XrSpatialComponentAnchorListEXT anchor_list = { XR_TYPE_SPATIAL_COMPONENT_ANCHOR_LIST_EXT, nullptr, 0, nullptr };
+};
+
+// Persistence configuration
+class OpenXRSpatialContextPersistenceConfig : public OpenXRStructureBase {
+	GDCLASS(OpenXRSpatialContextPersistenceConfig, OpenXRStructureBase);
+
+public:
+	bool has_valid_configuration() const;
+	virtual void *get_header(void *p_next) override;
+	virtual XrStructureType get_structure_type() override;
+
+	void add_persistence_context(RID p_persistence_context);
+	void remove_persistence_context(RID p_persistence_context);
+
+protected:
+	static void _bind_methods();
+
+private:
+	Vector<RID> persistence_contexts;
+	Vector<XrSpatialPersistenceContextEXT> context_handles;
+
+	XrSpatialContextPersistenceConfigEXT persistence_config = { XR_TYPE_SPATIAL_CONTEXT_PERSISTENCE_CONFIG_EXT, nullptr, 0, nullptr };
+};
+
+// Component persistence list
+class OpenXRSpatialComponentPersistenceList : public OpenXRSpatialComponentData {
+	GDCLASS(OpenXRSpatialComponentPersistenceList, OpenXRSpatialComponentData);
+
+protected:
+	static void _bind_methods();
+
+public:
+	virtual void set_capacity(uint32_t p_capacity) override;
+	virtual XrSpatialComponentTypeEXT get_component_type() const override;
+	virtual void *get_structure_data(void *p_next) override;
+
+	XrUuid get_persistent_uuid(int64_t p_index) const;
+	XrSpatialPersistenceStateEXT get_persistent_state(int64_t p_index) const;
+
+	static String get_persistence_state_name(XrSpatialPersistenceStateEXT p_state);
+
+private:
+	Vector<XrSpatialPersistenceDataEXT> persist_data;
+
+	XrSpatialComponentPersistenceListEXT persistence_list = { XR_TYPE_SPATIAL_COMPONENT_PERSISTENCE_LIST_EXT, nullptr, 0, nullptr };
+
+	String _get_persistent_uuid(int64_t p_index) const;
+	uint64_t _get_persistent_state(int64_t p_index) const;
+};
+
+// Anchor tracker, this adds no new logic, it's purely for typing!
+class OpenXRAnchorTracker : public OpenXRSpatialEntityTracker {
+	GDCLASS(OpenXRAnchorTracker, OpenXRSpatialEntityTracker);
+
+protected:
+	static void _bind_methods();
+
+public:
+	bool has_uuid() const;
+	XrUuid get_uuid() const;
+	void set_uuid(const XrUuid &p_uuid);
+
+private:
+	XrUuid uuid = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
+
+	String _get_uuid() const;
+	void _set_uuid(const String &p_uuid);
+
+	bool uuid_is_equal(const XrUuid &p_a, const XrUuid &p_b);
+};
+
+// (Persistent) anchor logic
+class OpenXRSpatialAnchorCapability : public OpenXRExtensionWrapper {
+	GDCLASS(OpenXRSpatialAnchorCapability, OpenXRExtensionWrapper);
+
+public:
+	enum PersistenceScope {
+		PERSISTENCE_SCOPE_SYSTEM_MANAGED = XR_SPATIAL_PERSISTENCE_SCOPE_SYSTEM_MANAGED_EXT,
+		PERSISTENCE_SCOPE_LOCAL_ANCHORS = XR_SPATIAL_PERSISTENCE_SCOPE_LOCAL_ANCHORS_EXT,
+	};
+
+	static OpenXRSpatialAnchorCapability *get_singleton();
+
+	OpenXRSpatialAnchorCapability();
+	virtual ~OpenXRSpatialAnchorCapability() override;
+
+	virtual HashMap<String, bool *> get_requested_extensions() override;
+
+	virtual void on_instance_created(const XrInstance p_instance) override;
+	virtual void on_instance_destroyed() override;
+	virtual void on_session_created(const XrSession p_session) override;
+	virtual void on_session_destroyed() override;
+
+	virtual void on_process() override;
+
+	bool is_spatial_anchor_supported();
+	bool is_spatial_persistence_supported();
+
+	// Persistence scopes
+	bool is_persistence_scope_supported(XrSpatialPersistenceScopeEXT p_scope);
+	Ref<OpenXRFutureResult> create_persistence_context(XrSpatialPersistenceScopeEXT p_scope, const Callable &p_user_callback = Callable());
+	XrSpatialPersistenceContextEXT get_persistence_context_handle(RID p_persistence_context) const;
+	void free_persistence_context(RID p_persistence_context);
+
+	Ref<OpenXRAnchorTracker> create_new_anchor(const Transform3D &p_transform, RID p_spatial_context = RID());
+	void remove_anchor(Ref<OpenXRAnchorTracker> p_anchor_tracker);
+	Ref<OpenXRFutureResult> persist_anchor(Ref<OpenXRAnchorTracker> p_anchor_tracker, RID p_persistence_context = RID(), const Callable &p_user_callback = Callable());
+	Ref<OpenXRFutureResult> unpersist_anchor(Ref<OpenXRAnchorTracker> p_anchor_tracker, RID p_persistence_context = RID(), const Callable &p_user_callback = Callable());
+
+	static String get_spatial_persistence_scope_name(XrSpatialPersistenceScopeEXT p_scope);
+	static String get_spatial_persistence_context_result_name(XrSpatialPersistenceContextResultEXT p_result);
+
+protected:
+	static void _bind_methods();
+
+private:
+	static OpenXRSpatialAnchorCapability *singleton;
+
+	bool spatial_anchor_ext = false;
+	bool spatial_persistence_ext = false;
+	bool spatial_persistence_operations_ext = false;
+	bool spatial_anchor_supported = false;
+
+	RID spatial_context;
+	RID persistence_context;
+	bool need_discovery = false;
+	int discovery_cooldown = 0;
+	Ref<OpenXRFutureResult> discovery_query_result;
+
+	Ref<OpenXRSpatialCapabilityConfigurationAnchor> anchor_configuration;
+	Ref<OpenXRSpatialContextPersistenceConfig> persistence_configuration;
+
+	Vector<XrSpatialPersistenceScopeEXT> supported_persistence_scopes;
+	bool _load_supported_persistence_scopes();
+
+	// Persistence scopes
+	struct PersistenceContextData {
+		XrSpatialPersistenceScopeEXT scope;
+		XrSpatialPersistenceContextEXT persistence_context = XR_NULL_HANDLE;
+	};
+	mutable RID_Owner<PersistenceContextData> persistence_context_owner;
+
+	bool _is_persistence_scope_supported(PersistenceScope p_scope);
+	Ref<OpenXRFutureResult> _create_persistence_context(PersistenceScope p_scope, Callable p_user_callback = Callable());
+
+	uint64_t _get_persistence_context_handle(RID p_persistence_context) const;
+	void _on_persistence_context_ready(Ref<OpenXRFutureResult> p_future_result, uint64_t p_scope, Callable p_user_callback = Callable());
+
+	// Discovery logic
+	void _on_persistence_context_completed(RID p_persistence_context);
+
+	Ref<OpenXRFutureResult> _create_spatial_context();
+	void _on_spatial_context_created(RID p_spatial_context);
+
+	void _on_spatial_discovery_recommended(RID p_spatial_context);
+
+	Ref<OpenXRFutureResult> _start_entity_discovery();
+	void _process_discovery_snapshot(RID p_snapshot);
+	void _process_update_snapshot(RID p_snapshot);
+
+	// Entities
+	void _on_made_anchor_persistent(Ref<OpenXRFutureResult> p_future_result, RID p_persistence_context, Ref<OpenXRAnchorTracker> p_anchor_tracker, const Callable &p_callback);
+	void _on_made_anchor_unpersistent(Ref<OpenXRFutureResult> p_future_result, RID p_persistence_context, Ref<OpenXRAnchorTracker> p_anchor_tracker, const Callable &p_callback);
+
+	// Trackers
+	HashMap<XrSpatialEntityIdEXT, Ref<OpenXRAnchorTracker>> anchors;
+
+	// OpenXR API call wrappers
+	EXT_PROTO_XRRESULT_FUNC4(xrCreateSpatialAnchorEXT, (XrSpatialContextEXT), spatialContext, (const XrSpatialAnchorCreateInfoEXT *), create_info, (XrSpatialEntityIdEXT *), anchor_entity_id, (XrSpatialEntityEXT *), anchor_entity);
+
+	EXT_PROTO_XRRESULT_FUNC5(xrEnumerateSpatialPersistenceScopesEXT, (XrInstance), instance, (XrSystemId), system_id, (uint32_t), persistence_scope_capacity_input, (uint32_t *), persistence_scope_count_output, (XrSpatialPersistenceScopeEXT *), persistence_scopes);
+	EXT_PROTO_XRRESULT_FUNC3(xrCreateSpatialPersistenceContextAsyncEXT, (XrSession), session, (const XrSpatialPersistenceContextCreateInfoEXT *), create_info, (XrFutureEXT *), future);
+	EXT_PROTO_XRRESULT_FUNC3(xrCreateSpatialPersistenceContextCompleteEXT, (XrSession), session, (XrFutureEXT), future, (XrCreateSpatialPersistenceContextCompletionEXT *), completion);
+	EXT_PROTO_XRRESULT_FUNC1(xrDestroySpatialPersistenceContextEXT, (XrSpatialPersistenceContextEXT), persistence_context);
+
+	EXT_PROTO_XRRESULT_FUNC3(xrPersistSpatialEntityAsyncEXT, (XrSpatialPersistenceContextEXT), persistence_context, (const XrSpatialEntityPersistInfoEXT *), persist_info, (XrFutureEXT *), future);
+	EXT_PROTO_XRRESULT_FUNC3(xrPersistSpatialEntityCompleteEXT, (XrSpatialPersistenceContextEXT), persistence_context, (XrFutureEXT), future, (XrPersistSpatialEntityCompletionEXT *), completion);
+	EXT_PROTO_XRRESULT_FUNC3(xrUnpersistSpatialEntityAsyncEXT, (XrSpatialPersistenceContextEXT), persistence_context, (const XrSpatialEntityUnpersistInfoEXT *), unpersist_info, (XrFutureEXT *), future);
+	EXT_PROTO_XRRESULT_FUNC3(xrUnpersistSpatialEntityCompleteEXT, (XrSpatialPersistenceContextEXT), persistence_context, (XrFutureEXT), future, (XrUnpersistSpatialEntityCompletionEXT *), completion);
+};
+
+VARIANT_ENUM_CAST(OpenXRSpatialAnchorCapability::PersistenceScope);

+ 504 - 0
modules/openxr/extensions/spatial_entities/openxr_spatial_entities.cpp

@@ -0,0 +1,504 @@
+/**************************************************************************/
+/*  openxr_spatial_entities.cpp                                           */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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 "openxr_spatial_entities.h"
+
+#include "../../openxr_api.h"
+#include "core/variant/native_ptr.h"
+#include "openxr_spatial_entity_extension.h"
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialCapabilityConfigurationBaseHeader
+
+void OpenXRSpatialCapabilityConfigurationBaseHeader::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("has_valid_configuration"), &OpenXRSpatialCapabilityConfigurationBaseHeader::has_valid_configuration);
+
+	GDVIRTUAL_BIND(_has_valid_configuration);
+	GDVIRTUAL_BIND(_get_configuration);
+}
+
+bool OpenXRSpatialCapabilityConfigurationBaseHeader::has_valid_configuration() const {
+	bool is_valid = false;
+
+	if (GDVIRTUAL_CALL(_has_valid_configuration, is_valid)) {
+		return is_valid;
+	}
+
+	return false;
+}
+
+XrSpatialCapabilityConfigurationBaseHeaderEXT *OpenXRSpatialCapabilityConfigurationBaseHeader::get_configuration() {
+	uint64_t pointer = 0;
+
+	if (GDVIRTUAL_CALL(_get_configuration, pointer)) {
+		return reinterpret_cast<XrSpatialCapabilityConfigurationBaseHeaderEXT *>(pointer);
+	}
+
+	return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialEntityTracker
+
+void OpenXRSpatialEntityTracker::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("set_entity", "entity"), &OpenXRSpatialEntityTracker::set_entity);
+	ClassDB::bind_method(D_METHOD("get_entity"), &OpenXRSpatialEntityTracker::get_entity);
+
+	ADD_PROPERTY(PropertyInfo(Variant::RID, "entity"), "set_entity", "get_entity");
+
+	ClassDB::bind_method(D_METHOD("set_spatial_tracking_state", "spatial_tracking_state"), &OpenXRSpatialEntityTracker::_set_spatial_tracking_state);
+	ClassDB::bind_method(D_METHOD("get_spatial_tracking_state"), &OpenXRSpatialEntityTracker::_get_spatial_tracking_state);
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "spatial_tracking_state"), "set_spatial_tracking_state", "get_spatial_tracking_state");
+
+	ADD_SIGNAL(MethodInfo("spatial_tracking_state_changed", PropertyInfo(Variant::INT, "spatial_tracking_state")));
+
+	BIND_ENUM_CONSTANT(ENTITY_TRACKING_STATE_STOPPED);
+	BIND_ENUM_CONSTANT(ENTITY_TRACKING_STATE_PAUSED);
+	BIND_ENUM_CONSTANT(ENTITY_TRACKING_STATE_TRACKING);
+}
+
+OpenXRSpatialEntityTracker::OpenXRSpatialEntityTracker() {
+	set_tracker_type(XRServer::TrackerType::TRACKER_ANCHOR);
+}
+
+OpenXRSpatialEntityTracker::~OpenXRSpatialEntityTracker() {
+	if (spatial_entity.is_valid()) {
+		OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+		if (se_extension) {
+			se_extension->free_spatial_entity(spatial_entity);
+			spatial_entity = RID();
+		}
+	}
+}
+
+void OpenXRSpatialEntityTracker::set_entity(const RID &p_entity) {
+	if (spatial_entity.is_valid()) {
+		if (spatial_entity == p_entity) {
+			return;
+		}
+
+		OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+		if (se_extension) {
+			se_extension->free_spatial_entity(spatial_entity);
+			spatial_entity = RID();
+		}
+	}
+
+	spatial_entity = p_entity;
+
+	if (p_entity.is_valid()) {
+		OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+		ERR_FAIL_NULL(se_extension);
+
+		XrSpatialEntityIdEXT entity_id = se_extension->get_spatial_entity_id(p_entity);
+
+		String tracker_name = String("openxr/spatial_entity/") + String::num_int64(entity_id);
+		set_tracker_name(tracker_name);
+	} else {
+		set_tracker_name("openxr/spatial_entity/null");
+	}
+}
+
+RID OpenXRSpatialEntityTracker::get_entity() const {
+	return spatial_entity;
+}
+
+void OpenXRSpatialEntityTracker::set_spatial_tracking_state(const XrSpatialEntityTrackingStateEXT p_state) {
+	if (spatial_tracking_state != p_state) {
+		spatial_tracking_state = p_state;
+
+		emit_signal(SNAME("spatial_tracking_state_changed"), spatial_tracking_state);
+	}
+}
+
+void OpenXRSpatialEntityTracker::_set_spatial_tracking_state(const EntityTrackingState p_state) {
+	set_spatial_tracking_state((XrSpatialEntityTrackingStateEXT)p_state);
+}
+
+XrSpatialEntityTrackingStateEXT OpenXRSpatialEntityTracker::get_spatial_tracking_state() const {
+	return spatial_tracking_state;
+}
+
+OpenXRSpatialEntityTracker::EntityTrackingState OpenXRSpatialEntityTracker::_get_spatial_tracking_state() const {
+	return (EntityTrackingState)get_spatial_tracking_state();
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialComponentData
+
+void OpenXRSpatialComponentData::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("set_capacity", "capacity"), &OpenXRSpatialComponentData::set_capacity);
+
+	GDVIRTUAL_BIND(_set_capacity, "capacity");
+	GDVIRTUAL_BIND(_get_component_type);
+	GDVIRTUAL_BIND(_get_structure_data, "next");
+}
+
+void OpenXRSpatialComponentData::set_capacity(uint32_t p_capacity) {
+	GDVIRTUAL_CALL(_set_capacity, p_capacity);
+}
+
+XrSpatialComponentTypeEXT OpenXRSpatialComponentData::get_component_type() const {
+	uint64_t component_type = XR_SPATIAL_COMPONENT_TYPE_MAX_ENUM_EXT;
+
+	if (GDVIRTUAL_CALL(_get_component_type, component_type)) {
+		return (XrSpatialComponentTypeEXT)component_type;
+	}
+
+	return XR_SPATIAL_COMPONENT_TYPE_MAX_ENUM_EXT;
+}
+
+void *OpenXRSpatialComponentData::get_structure_data(void *p_next) {
+	uint64_t pointer = 0;
+
+	if (GDVIRTUAL_CALL(_get_structure_data, (uint64_t)p_next, pointer)) {
+		return reinterpret_cast<void *>(pointer);
+	}
+
+	return p_next;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Spatial component bounded2d list
+
+void OpenXRSpatialComponentBounded2DList::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_center_pose", "index"), &OpenXRSpatialComponentBounded2DList::get_center_pose);
+	ClassDB::bind_method(D_METHOD("get_size", "index"), &OpenXRSpatialComponentBounded2DList::get_size);
+}
+
+void OpenXRSpatialComponentBounded2DList::set_capacity(uint32_t p_capacity) {
+	bounded2d_data.resize(p_capacity);
+
+	bounded2d_list.boundCount = uint32_t(bounded2d_data.size());
+	bounded2d_list.bounds = bounded2d_data.ptrw();
+}
+
+XrSpatialComponentTypeEXT OpenXRSpatialComponentBounded2DList::get_component_type() const {
+	return XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT;
+}
+
+void *OpenXRSpatialComponentBounded2DList::get_structure_data(void *p_next) {
+	bounded2d_list.next = p_next;
+	return &bounded2d_list;
+}
+
+Transform3D OpenXRSpatialComponentBounded2DList::get_center_pose(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, bounded2d_data.size(), Transform3D());
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, Transform3D());
+
+	return openxr_api->transform_from_pose(bounded2d_data[p_index].center);
+}
+
+Vector2 OpenXRSpatialComponentBounded2DList::get_size(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, bounded2d_data.size(), Vector2());
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, Vector2());
+
+	const XrExtent2Df &extents = bounded2d_data[p_index].extents;
+	return Vector2(extents.width, extents.height);
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Spatial component bounded3d list
+
+void OpenXRSpatialComponentBounded3DList::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_center_pose", "index"), &OpenXRSpatialComponentBounded3DList::get_center_pose);
+	ClassDB::bind_method(D_METHOD("get_size", "index"), &OpenXRSpatialComponentBounded3DList::get_size);
+}
+
+void OpenXRSpatialComponentBounded3DList::set_capacity(uint32_t p_capacity) {
+	bounded3d_data.resize(p_capacity);
+
+	bounded3d_list.boundCount = uint32_t(bounded3d_data.size());
+	bounded3d_list.bounds = bounded3d_data.ptrw();
+}
+
+XrSpatialComponentTypeEXT OpenXRSpatialComponentBounded3DList::get_component_type() const {
+	return XR_SPATIAL_COMPONENT_TYPE_BOUNDED_3D_EXT;
+}
+
+void *OpenXRSpatialComponentBounded3DList::get_structure_data(void *p_next) {
+	bounded3d_list.next = p_next;
+	return &bounded3d_list;
+}
+
+Transform3D OpenXRSpatialComponentBounded3DList::get_center_pose(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, bounded3d_data.size(), Transform3D());
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, Transform3D());
+
+	return openxr_api->transform_from_pose(bounded3d_data[p_index].center);
+}
+
+Vector3 OpenXRSpatialComponentBounded3DList::get_size(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, bounded3d_data.size(), Vector3());
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, Vector3());
+
+	const XrExtent3Df &extents = bounded3d_data[p_index].extents;
+	return Vector3(extents.width, extents.height, extents.depth);
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Spatial component parent list
+
+void OpenXRSpatialComponentParentList::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_parent", "index"), &OpenXRSpatialComponentParentList::get_parent);
+}
+
+void OpenXRSpatialComponentParentList::set_capacity(uint32_t p_capacity) {
+	parent_data.resize(p_capacity);
+
+	parent_list.parentCount = uint32_t(parent_data.size());
+	parent_list.parents = parent_data.ptrw();
+}
+
+XrSpatialComponentTypeEXT OpenXRSpatialComponentParentList::get_component_type() const {
+	return XR_SPATIAL_COMPONENT_TYPE_PARENT_EXT;
+}
+
+void *OpenXRSpatialComponentParentList::get_structure_data(void *p_next) {
+	parent_list.next = p_next;
+	return &parent_list;
+}
+
+RID OpenXRSpatialComponentParentList::get_parent(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, parent_data.size(), RID());
+
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, RID());
+
+	return se_extension->find_spatial_entity(parent_data[p_index]);
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Spatial component mesh2d list
+
+void OpenXRSpatialComponentMesh2DList::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_transform", "index"), &OpenXRSpatialComponentMesh2DList::get_transform);
+	ClassDB::bind_method(D_METHOD("get_vertices", "snapshot", "index"), &OpenXRSpatialComponentMesh2DList::get_vertices);
+	ClassDB::bind_method(D_METHOD("get_indices", "snapshot", "index"), &OpenXRSpatialComponentMesh2DList::get_indices);
+}
+
+void OpenXRSpatialComponentMesh2DList::set_capacity(uint32_t p_capacity) {
+	mesh2d_data.resize(p_capacity);
+
+	mesh2d_list.meshCount = uint32_t(mesh2d_data.size());
+	mesh2d_list.meshes = mesh2d_data.ptrw();
+}
+
+XrSpatialComponentTypeEXT OpenXRSpatialComponentMesh2DList::get_component_type() const {
+	return XR_SPATIAL_COMPONENT_TYPE_MESH_2D_EXT;
+}
+
+void *OpenXRSpatialComponentMesh2DList::get_structure_data(void *p_next) {
+	mesh2d_list.next = p_next;
+	return &mesh2d_list;
+}
+
+Transform3D OpenXRSpatialComponentMesh2DList::get_transform(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, mesh2d_data.size(), Transform3D());
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, Transform3D());
+
+	return openxr_api->transform_from_pose(mesh2d_data[p_index].origin);
+}
+
+PackedVector2Array OpenXRSpatialComponentMesh2DList::get_vertices(RID p_snapshot, int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, mesh2d_data.size(), PackedVector2Array());
+
+	const XrSpatialBufferEXT &buffer = mesh2d_data[p_index].vertexBuffer;
+	if (buffer.bufferId == XR_NULL_SPATIAL_BUFFER_ID_EXT) {
+		// We don't have data (yet).
+		return PackedVector2Array();
+	}
+
+	ERR_FAIL_COND_V(buffer.bufferType != XR_SPATIAL_BUFFER_TYPE_VECTOR2F_EXT, PackedVector2Array());
+
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, PackedVector2Array());
+
+	return se_extension->get_vector2_buffer(p_snapshot, buffer.bufferId);
+}
+
+PackedInt32Array OpenXRSpatialComponentMesh2DList::get_indices(RID p_snapshot, int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, mesh2d_data.size(), PackedInt32Array());
+
+	const XrSpatialBufferEXT &buffer = mesh2d_data[p_index].indexBuffer;
+	if (buffer.bufferId == XR_NULL_SPATIAL_BUFFER_ID_EXT) {
+		// We don't have data (yet).
+		return PackedInt32Array();
+	}
+
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, PackedInt32Array());
+
+	PackedInt32Array ret;
+
+	switch (buffer.bufferType) {
+		case XR_SPATIAL_BUFFER_TYPE_UINT8_EXT: {
+			PackedByteArray data = se_extension->get_uint8_buffer(p_snapshot, buffer.bufferId);
+
+			ret.resize(data.size());
+			int count = ret.size();
+			int32_t *ptr = ret.ptrw();
+			for (int i = 0; i < count; i++) {
+				ptr[i] = data[i];
+			}
+		} break;
+		case XR_SPATIAL_BUFFER_TYPE_UINT16_EXT: {
+			Vector<uint16_t> data = se_extension->get_uint16_buffer(p_snapshot, buffer.bufferId);
+
+			ret.resize(data.size());
+			int count = ret.size();
+			int32_t *ptr = ret.ptrw();
+			for (int i = 0; i < count; i++) {
+				ptr[i] = data[i];
+			}
+		} break;
+		case XR_SPATIAL_BUFFER_TYPE_UINT32_EXT: {
+			Vector<uint32_t> data = se_extension->get_uint32_buffer(p_snapshot, buffer.bufferId);
+
+			ret.resize(data.size());
+			int count = ret.size();
+			int32_t *ptr = ret.ptrw();
+			for (int i = 0; i < count; i++) {
+				ptr[i] = data[i];
+			}
+		} break;
+		default: {
+			ERR_FAIL_V_MSG(PackedInt32Array(), "OpenXR: Unsupported buffer type for indices.");
+		} break;
+	}
+
+	return ret;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Spatial component mesh3d list
+
+void OpenXRSpatialComponentMesh3DList::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_transform", "index"), &OpenXRSpatialComponentMesh3DList::get_transform);
+	ClassDB::bind_method(D_METHOD("get_mesh", "index"), &OpenXRSpatialComponentMesh3DList::get_mesh);
+}
+
+void OpenXRSpatialComponentMesh3DList::set_capacity(uint32_t p_capacity) {
+	mesh3d_data.resize(p_capacity);
+
+	mesh3d_list.meshCount = uint32_t(mesh3d_data.size());
+	mesh3d_list.meshes = mesh3d_data.ptrw();
+}
+
+XrSpatialComponentTypeEXT OpenXRSpatialComponentMesh3DList::get_component_type() const {
+	return XR_SPATIAL_COMPONENT_TYPE_MESH_3D_EXT;
+}
+
+void *OpenXRSpatialComponentMesh3DList::get_structure_data(void *p_next) {
+	mesh3d_list.next = p_next;
+	return &mesh3d_list;
+}
+
+Transform3D OpenXRSpatialComponentMesh3DList::get_transform(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, mesh3d_data.size(), Transform3D());
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, Transform3D());
+
+	return openxr_api->transform_from_pose(mesh3d_data[p_index].origin);
+}
+
+Ref<Mesh> OpenXRSpatialComponentMesh3DList::get_mesh(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, mesh3d_data.size(), nullptr);
+
+	// TODO implement, need to convert mesh data to Godot mesh resource
+
+	return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialQueryResultData
+
+void OpenXRSpatialQueryResultData::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_capacity"), &OpenXRSpatialQueryResultData::get_capacity);
+	ClassDB::bind_method(D_METHOD("get_entity_id", "index"), &OpenXRSpatialQueryResultData::_get_entity_id);
+	ClassDB::bind_method(D_METHOD("get_entity_state", "index"), &OpenXRSpatialQueryResultData::_get_entity_state);
+}
+
+void OpenXRSpatialQueryResultData::set_capacity(uint32_t p_capacity) {
+	entity_ids.resize(p_capacity);
+	entity_states.resize(p_capacity);
+
+	query_result.entityIdCapacityInput = entity_ids.size();
+	query_result.entityIds = entity_ids.ptrw();
+	query_result.entityStateCapacityInput = entity_states.size();
+	query_result.entityStates = entity_states.ptrw();
+}
+
+XrSpatialComponentTypeEXT OpenXRSpatialQueryResultData::get_component_type() const {
+	// This component is always included and has no type.
+	return XR_SPATIAL_COMPONENT_TYPE_MAX_ENUM_EXT;
+}
+
+void *OpenXRSpatialQueryResultData::get_structure_data(void *p_next) {
+	query_result.next = p_next;
+	query_result.entityIdCountOutput = 0;
+	query_result.entityStateCountOutput = 0;
+	return &query_result;
+}
+
+XrSpatialEntityIdEXT OpenXRSpatialQueryResultData::get_entity_id(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, entity_ids.size(), XR_NULL_ENTITY);
+
+	return entity_ids[p_index];
+}
+
+uint64_t OpenXRSpatialQueryResultData::_get_entity_id(int64_t p_index) const {
+	return (uint64_t)get_entity_id(p_index);
+}
+
+XrSpatialEntityTrackingStateEXT OpenXRSpatialQueryResultData::get_entity_state(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, entity_states.size(), XR_SPATIAL_ENTITY_TRACKING_STATE_MAX_ENUM_EXT);
+
+	return entity_states[p_index];
+}
+
+OpenXRSpatialEntityTracker::EntityTrackingState OpenXRSpatialQueryResultData::_get_entity_state(int64_t p_index) const {
+	return (OpenXRSpatialEntityTracker::EntityTrackingState)get_entity_state(p_index);
+}
+
+String OpenXRSpatialQueryResultData::get_entity_tracking_state_name(XrSpatialEntityTrackingStateEXT p_tracking_state) {
+	XR_ENUM_SWITCH(XrSpatialEntityTrackingStateEXT, p_tracking_state)
+}

+ 230 - 0
modules/openxr/extensions/spatial_entities/openxr_spatial_entities.h

@@ -0,0 +1,230 @@
+/**************************************************************************/
+/*  openxr_spatial_entities.h                                             */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "../../openxr_structure.h"
+#include "../openxr_future_extension.h"
+#include "scene/resources/mesh.h"
+#include "servers/xr/xr_positional_tracker.h"
+
+#define XR_NULL_ENTITY 0x7FFFFFFF
+
+// Wrapper class for XrSpatialCapabilityConfigurationBaseHeaderEXT
+class OpenXRSpatialCapabilityConfigurationBaseHeader : public RefCounted {
+	GDCLASS(OpenXRSpatialCapabilityConfigurationBaseHeader, RefCounted);
+
+protected:
+	static void _bind_methods();
+
+public:
+	virtual bool has_valid_configuration() const;
+	virtual XrSpatialCapabilityConfigurationBaseHeaderEXT *get_configuration();
+
+	GDVIRTUAL0RC(bool, _has_valid_configuration);
+	GDVIRTUAL0R(uint64_t, _get_configuration);
+};
+
+// Tracker for our spatial entities
+class OpenXRSpatialEntityTracker : public XRPositionalTracker {
+	GDCLASS(OpenXRSpatialEntityTracker, XRPositionalTracker);
+
+public:
+	enum EntityTrackingState {
+		ENTITY_TRACKING_STATE_STOPPED = XR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED_EXT,
+		ENTITY_TRACKING_STATE_PAUSED = XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT,
+		ENTITY_TRACKING_STATE_TRACKING = XR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING_EXT,
+	};
+
+	OpenXRSpatialEntityTracker();
+	virtual ~OpenXRSpatialEntityTracker();
+
+	void set_entity(const RID &p_entity);
+	RID get_entity() const;
+
+	void set_spatial_tracking_state(const XrSpatialEntityTrackingStateEXT p_state);
+	XrSpatialEntityTrackingStateEXT get_spatial_tracking_state() const;
+
+protected:
+	static void _bind_methods();
+
+private:
+	RID spatial_entity;
+	XrSpatialEntityTrackingStateEXT spatial_tracking_state = XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT;
+
+	void _set_spatial_tracking_state(const EntityTrackingState p_state);
+	EntityTrackingState _get_spatial_tracking_state() const;
+};
+
+VARIANT_ENUM_CAST(OpenXRSpatialEntityTracker::EntityTrackingState)
+
+// Wrapper class for our spatial component data returned by discovery queries
+class OpenXRSpatialComponentData : public RefCounted {
+	GDCLASS(OpenXRSpatialComponentData, RefCounted);
+
+protected:
+	static void _bind_methods();
+
+public:
+	virtual void set_capacity(uint32_t p_capacity);
+	virtual XrSpatialComponentTypeEXT get_component_type() const;
+	virtual void *get_structure_data(void *p_next);
+
+	GDVIRTUAL1(_set_capacity, uint32_t);
+	GDVIRTUAL0RC(uint64_t, _get_component_type);
+	GDVIRTUAL1RC(uint64_t, _get_structure_data, uint64_t);
+};
+
+class OpenXRSpatialComponentBounded2DList : public OpenXRSpatialComponentData {
+	GDCLASS(OpenXRSpatialComponentBounded2DList, OpenXRSpatialComponentData);
+
+protected:
+	static void _bind_methods();
+
+public:
+	virtual void set_capacity(uint32_t p_capacity) override;
+	virtual XrSpatialComponentTypeEXT get_component_type() const override;
+	virtual void *get_structure_data(void *p_next) override;
+
+	Transform3D get_center_pose(int64_t p_index) const;
+	Vector2 get_size(int64_t p_index) const;
+
+private:
+	Vector<XrSpatialBounded2DDataEXT> bounded2d_data;
+
+	XrSpatialComponentBounded2DListEXT bounded2d_list = { XR_TYPE_SPATIAL_COMPONENT_BOUNDED_2D_LIST_EXT, nullptr, 0, nullptr };
+};
+
+class OpenXRSpatialComponentBounded3DList : public OpenXRSpatialComponentData {
+	GDCLASS(OpenXRSpatialComponentBounded3DList, OpenXRSpatialComponentData);
+
+protected:
+	static void _bind_methods();
+
+public:
+	virtual void set_capacity(uint32_t p_capacity) override;
+	virtual XrSpatialComponentTypeEXT get_component_type() const override;
+	virtual void *get_structure_data(void *p_next) override;
+
+	Transform3D get_center_pose(int64_t p_index) const;
+	Vector3 get_size(int64_t p_index) const;
+
+private:
+	Vector<XrBoxf> bounded3d_data;
+
+	XrSpatialComponentBounded3DListEXT bounded3d_list = { XR_TYPE_SPATIAL_COMPONENT_BOUNDED_3D_LIST_EXT, nullptr, 0, nullptr };
+};
+
+class OpenXRSpatialComponentParentList : public OpenXRSpatialComponentData {
+	GDCLASS(OpenXRSpatialComponentParentList, OpenXRSpatialComponentData);
+
+protected:
+	static void _bind_methods();
+
+public:
+	virtual void set_capacity(uint32_t p_capacity) override;
+	virtual XrSpatialComponentTypeEXT get_component_type() const override;
+	virtual void *get_structure_data(void *p_next) override;
+
+	RID get_parent(int64_t p_index) const;
+
+private:
+	Vector<XrSpatialEntityIdEXT> parent_data;
+
+	XrSpatialComponentParentListEXT parent_list = { XR_TYPE_SPATIAL_COMPONENT_PARENT_LIST_EXT, nullptr, 0, nullptr };
+};
+
+class OpenXRSpatialComponentMesh2DList : public OpenXRSpatialComponentData {
+	GDCLASS(OpenXRSpatialComponentMesh2DList, OpenXRSpatialComponentData);
+
+protected:
+	static void _bind_methods();
+
+public:
+	virtual void set_capacity(uint32_t p_capacity) override;
+	virtual XrSpatialComponentTypeEXT get_component_type() const override;
+	virtual void *get_structure_data(void *p_next) override;
+
+	Transform3D get_transform(int64_t p_index) const;
+	PackedVector2Array get_vertices(RID p_snapshot, int64_t p_index) const;
+	PackedInt32Array get_indices(RID p_snapshot, int64_t p_index) const;
+
+private:
+	Vector<XrSpatialMeshDataEXT> mesh2d_data;
+
+	XrSpatialComponentMesh2DListEXT mesh2d_list = { XR_TYPE_SPATIAL_COMPONENT_MESH_2D_LIST_EXT, nullptr, 0, nullptr };
+};
+
+class OpenXRSpatialComponentMesh3DList : public OpenXRSpatialComponentData {
+	GDCLASS(OpenXRSpatialComponentMesh3DList, OpenXRSpatialComponentData);
+
+protected:
+	static void _bind_methods();
+
+public:
+	virtual void set_capacity(uint32_t p_capacity) override;
+	virtual XrSpatialComponentTypeEXT get_component_type() const override;
+	virtual void *get_structure_data(void *p_next) override;
+
+	Transform3D get_transform(int64_t p_index) const;
+	Ref<Mesh> get_mesh(int64_t p_index) const;
+
+private:
+	Vector<XrSpatialMeshDataEXT> mesh3d_data;
+
+	XrSpatialComponentMesh3DListEXT mesh3d_list = { XR_TYPE_SPATIAL_COMPONENT_MESH_3D_LIST_EXT, nullptr, 0, nullptr };
+};
+
+class OpenXRSpatialQueryResultData : public OpenXRSpatialComponentData {
+	GDCLASS(OpenXRSpatialQueryResultData, OpenXRSpatialComponentData);
+
+protected:
+	static void _bind_methods();
+
+public:
+	virtual void set_capacity(uint32_t p_capacity) override;
+	virtual XrSpatialComponentTypeEXT get_component_type() const override;
+	virtual void *get_structure_data(void *p_next) override;
+
+	int64_t get_capacity() const { return entity_ids.size(); }
+	XrSpatialEntityIdEXT get_entity_id(int64_t p_index) const;
+	XrSpatialEntityTrackingStateEXT get_entity_state(int64_t p_index) const;
+
+	static String get_entity_tracking_state_name(XrSpatialEntityTrackingStateEXT p_tracking_state);
+
+private:
+	Vector<XrSpatialEntityIdEXT> entity_ids;
+	Vector<XrSpatialEntityTrackingStateEXT> entity_states;
+
+	XrSpatialComponentDataQueryResultEXT query_result = { XR_TYPE_SPATIAL_COMPONENT_DATA_QUERY_RESULT_EXT, nullptr, 0, 0, nullptr, 0, 0, nullptr };
+
+	uint64_t _get_entity_id(int64_t p_index) const;
+	OpenXRSpatialEntityTracker::EntityTrackingState _get_entity_state(int64_t p_index) const;
+};

+ 1215 - 0
modules/openxr/extensions/spatial_entities/openxr_spatial_entity_extension.cpp

@@ -0,0 +1,1215 @@
+/**************************************************************************/
+/*  openxr_spatial_entity_extension.cpp                                   */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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 "openxr_spatial_entity_extension.h"
+
+#include "../../openxr_api.h"
+#include "core/config/project_settings.h"
+#include "servers/xr_server.h"
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialEntityExtension
+
+OpenXRSpatialEntityExtension *OpenXRSpatialEntityExtension::singleton = nullptr;
+
+OpenXRSpatialEntityExtension *OpenXRSpatialEntityExtension::get_singleton() {
+	return singleton;
+}
+
+void OpenXRSpatialEntityExtension::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("supports_capability", "capability"), &OpenXRSpatialEntityExtension::_supports_capability);
+	ClassDB::bind_method(D_METHOD("supports_component_type", "capability", "component_type"), &OpenXRSpatialEntityExtension::_supports_component_type);
+
+	ClassDB::bind_method(D_METHOD("create_spatial_context", "capability_configurations", "next", "user_callback"), &OpenXRSpatialEntityExtension::create_spatial_context, DEFVAL(Variant()), DEFVAL(Callable()));
+	ClassDB::bind_method(D_METHOD("get_spatial_context_ready", "spatial_context"), &OpenXRSpatialEntityExtension::get_spatial_context_ready);
+	ClassDB::bind_method(D_METHOD("free_spatial_context", "spatial_context"), &OpenXRSpatialEntityExtension::free_spatial_context);
+	ClassDB::bind_method(D_METHOD("get_spatial_context_handle", "spatial_context"), &OpenXRSpatialEntityExtension::_get_spatial_context_handle);
+
+	ADD_SIGNAL(MethodInfo("spatial_discovery_recommended", PropertyInfo(Variant::RID, "spatial_context")));
+
+	// Component_types should be an int array typed to ComponentType(XrSpatialComponentTypeEXT), but we currently don't support that.
+	ClassDB::bind_method(D_METHOD("discover_spatial_entities", "spatial_context", "component_types", "next", "user_callback"), &OpenXRSpatialEntityExtension::_discover_spatial_entities, DEFVAL(Variant()), DEFVAL(Callable()));
+	ClassDB::bind_method(D_METHOD("update_spatial_entities", "spatial_context", "entities", "component_types", "next"), &OpenXRSpatialEntityExtension::_update_spatial_entities, DEFVAL(Variant()));
+
+	ClassDB::bind_method(D_METHOD("free_spatial_snapshot", "spatial_snapshot"), &OpenXRSpatialEntityExtension::free_spatial_snapshot);
+	ClassDB::bind_method(D_METHOD("get_spatial_snapshot_handle", "spatial_snapshot"), &OpenXRSpatialEntityExtension::_get_spatial_snapshot_handle);
+	ClassDB::bind_method(D_METHOD("get_spatial_snapshot_context", "spatial_snapshot"), &OpenXRSpatialEntityExtension::get_spatial_snapshot_context);
+	ClassDB::bind_method(D_METHOD("query_snapshot", "spatial_snapshot", "component_data", "next"), &OpenXRSpatialEntityExtension::query_snapshot, DEFVAL(Variant()));
+
+	ClassDB::bind_method(D_METHOD("get_string", "spatial_snapshot", "buffer_id"), &OpenXRSpatialEntityExtension::_get_string);
+	ClassDB::bind_method(D_METHOD("get_uint8_buffer", "spatial_snapshot", "buffer_id"), &OpenXRSpatialEntityExtension::_get_uint8_buffer);
+	ClassDB::bind_method(D_METHOD("get_uint16_buffer", "spatial_snapshot", "buffer_id"), &OpenXRSpatialEntityExtension::_get_uint16_buffer);
+	ClassDB::bind_method(D_METHOD("get_uint32_buffer", "spatial_snapshot", "buffer_id"), &OpenXRSpatialEntityExtension::_get_uint32_buffer);
+	ClassDB::bind_method(D_METHOD("get_float_buffer", "spatial_snapshot", "buffer_id"), &OpenXRSpatialEntityExtension::_get_float_buffer);
+	ClassDB::bind_method(D_METHOD("get_vector2_buffer", "spatial_snapshot", "buffer_id"), &OpenXRSpatialEntityExtension::_get_vector2_buffer);
+	ClassDB::bind_method(D_METHOD("get_vector3_buffer", "spatial_snapshot", "buffer_id"), &OpenXRSpatialEntityExtension::_get_vector3_buffer);
+
+	ClassDB::bind_method(D_METHOD("find_spatial_entity", "entity_id"), &OpenXRSpatialEntityExtension::_find_entity);
+	ClassDB::bind_method(D_METHOD("add_spatial_entity", "spatial_context", "entity_id", "entity"), &OpenXRSpatialEntityExtension::_add_entity);
+	ClassDB::bind_method(D_METHOD("make_spatial_entity", "spatial_context", "entity_id"), &OpenXRSpatialEntityExtension::_make_entity);
+	ClassDB::bind_method(D_METHOD("get_spatial_entity_id", "entity"), &OpenXRSpatialEntityExtension::_get_entity_id);
+	ClassDB::bind_method(D_METHOD("get_spatial_entity_context", "entity"), &OpenXRSpatialEntityExtension::get_spatial_entity_context);
+	ClassDB::bind_method(D_METHOD("free_spatial_entity", "entity"), &OpenXRSpatialEntityExtension::free_spatial_entity);
+
+	BIND_ENUM_CONSTANT(CAPABILITY_PLANE_TRACKING);
+	BIND_ENUM_CONSTANT(CAPABILITY_MARKER_TRACKING_QR_CODE);
+	BIND_ENUM_CONSTANT(CAPABILITY_MARKER_TRACKING_MICRO_QR_CODE);
+	BIND_ENUM_CONSTANT(CAPABILITY_MARKER_TRACKING_ARUCO_MARKER);
+	BIND_ENUM_CONSTANT(CAPABILITY_MARKER_TRACKING_APRIL_TAG);
+	BIND_ENUM_CONSTANT(CAPABILITY_ANCHOR);
+
+	BIND_ENUM_CONSTANT(COMPONENT_TYPE_BOUNDED_2D);
+	BIND_ENUM_CONSTANT(COMPONENT_TYPE_BOUNDED_3D);
+	BIND_ENUM_CONSTANT(COMPONENT_TYPE_PARENT);
+	BIND_ENUM_CONSTANT(COMPONENT_TYPE_MESH_3D);
+	BIND_ENUM_CONSTANT(COMPONENT_TYPE_PLANE_ALIGNMENT);
+	BIND_ENUM_CONSTANT(COMPONENT_TYPE_MESH_2D);
+	BIND_ENUM_CONSTANT(COMPONENT_TYPE_POLYGON_2D);
+	BIND_ENUM_CONSTANT(COMPONENT_TYPE_PLANE_SEMANTIC_LABEL);
+	BIND_ENUM_CONSTANT(COMPONENT_TYPE_MARKER);
+	BIND_ENUM_CONSTANT(COMPONENT_TYPE_ANCHOR);
+	BIND_ENUM_CONSTANT(COMPONENT_TYPE_PERSISTENCE);
+}
+
+OpenXRSpatialEntityExtension::OpenXRSpatialEntityExtension() {
+	singleton = this;
+}
+
+OpenXRSpatialEntityExtension::~OpenXRSpatialEntityExtension() {
+	singleton = nullptr;
+}
+
+HashMap<String, bool *> OpenXRSpatialEntityExtension::get_requested_extensions() {
+	HashMap<String, bool *> request_extensions;
+
+	if (GLOBAL_GET_CACHED(bool, "xr/openxr/extensions/spatial_entity/enabled")) {
+		request_extensions[XR_EXT_SPATIAL_ENTITY_EXTENSION_NAME] = &spatial_entity_ext;
+	}
+
+	return request_extensions;
+}
+
+void OpenXRSpatialEntityExtension::on_instance_created(const XrInstance p_instance) {
+	if (spatial_entity_ext) {
+		EXT_INIT_XR_FUNC(xrEnumerateSpatialCapabilitiesEXT);
+		EXT_INIT_XR_FUNC(xrEnumerateSpatialCapabilityComponentTypesEXT);
+		EXT_INIT_XR_FUNC(xrEnumerateSpatialCapabilityFeaturesEXT);
+		EXT_INIT_XR_FUNC(xrCreateSpatialContextAsyncEXT);
+		EXT_INIT_XR_FUNC(xrCreateSpatialContextCompleteEXT);
+		EXT_INIT_XR_FUNC(xrDestroySpatialContextEXT);
+		EXT_INIT_XR_FUNC(xrCreateSpatialDiscoverySnapshotAsyncEXT);
+		EXT_INIT_XR_FUNC(xrCreateSpatialDiscoverySnapshotCompleteEXT);
+		EXT_INIT_XR_FUNC(xrQuerySpatialComponentDataEXT);
+		EXT_INIT_XR_FUNC(xrDestroySpatialSnapshotEXT);
+		EXT_INIT_XR_FUNC(xrCreateSpatialEntityFromIdEXT);
+		EXT_INIT_XR_FUNC(xrDestroySpatialEntityEXT);
+		EXT_INIT_XR_FUNC(xrCreateSpatialUpdateSnapshotEXT);
+		EXT_INIT_XR_FUNC(xrGetSpatialBufferStringEXT);
+		EXT_INIT_XR_FUNC(xrGetSpatialBufferUint8EXT);
+		EXT_INIT_XR_FUNC(xrGetSpatialBufferUint16EXT);
+		EXT_INIT_XR_FUNC(xrGetSpatialBufferUint32EXT);
+		EXT_INIT_XR_FUNC(xrGetSpatialBufferFloatEXT);
+		EXT_INIT_XR_FUNC(xrGetSpatialBufferVector2fEXT);
+		EXT_INIT_XR_FUNC(xrGetSpatialBufferVector3fEXT);
+	}
+}
+
+void OpenXRSpatialEntityExtension::on_instance_destroyed() {
+	supported_capabilities.clear();
+	capabilities_load_state = 0;
+
+	xrEnumerateSpatialCapabilitiesEXT_ptr = nullptr;
+	xrEnumerateSpatialCapabilityComponentTypesEXT_ptr = nullptr;
+	xrEnumerateSpatialCapabilityFeaturesEXT_ptr = nullptr;
+	xrCreateSpatialContextAsyncEXT_ptr = nullptr;
+	xrCreateSpatialContextCompleteEXT_ptr = nullptr;
+	xrDestroySpatialContextEXT_ptr = nullptr;
+	xrCreateSpatialDiscoverySnapshotAsyncEXT_ptr = nullptr;
+	xrCreateSpatialDiscoverySnapshotCompleteEXT_ptr = nullptr;
+	xrQuerySpatialComponentDataEXT_ptr = nullptr;
+	xrDestroySpatialSnapshotEXT_ptr = nullptr;
+	xrCreateSpatialEntityFromIdEXT_ptr = nullptr;
+	xrDestroySpatialEntityEXT_ptr = nullptr;
+	xrCreateSpatialUpdateSnapshotEXT_ptr = nullptr;
+	xrGetSpatialBufferStringEXT_ptr = nullptr;
+	xrGetSpatialBufferUint8EXT_ptr = nullptr;
+	xrGetSpatialBufferUint16EXT_ptr = nullptr;
+	xrGetSpatialBufferUint32EXT_ptr = nullptr;
+	xrGetSpatialBufferFloatEXT_ptr = nullptr;
+	xrGetSpatialBufferVector2fEXT_ptr = nullptr;
+	xrGetSpatialBufferVector3fEXT_ptr = nullptr;
+}
+
+void OpenXRSpatialEntityExtension::on_session_destroyed() {
+	if (!get_active()) {
+		return;
+	}
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+
+	// Cleanup remaining entity RIDs.
+	LocalVector<RID> spatial_entity_rids = spatial_entity_owner.get_owned_list();
+	for (const RID &rid : spatial_entity_rids) {
+		if (is_print_verbose_enabled()) {
+			SpatialEntityData *spatial_entity_data = spatial_entity_owner.get_or_null(rid);
+			if (spatial_entity_data) { // Should never be nullptr seeing we called get_owned_list just now, but just in case.
+				print_line("OpenXR: Found orphaned spatial entity with ID ", String::num_int64(spatial_entity_data->entity_id));
+			}
+		}
+
+		free_spatial_entity(rid);
+	}
+
+	// Cleanup remaining snapshot RIDs.
+	LocalVector<RID> spatial_snapshot_rids = spatial_snapshot_owner.get_owned_list();
+	if (!spatial_snapshot_rids.is_empty()) {
+		print_verbose("OpenXR: Found " + String::num_int64(spatial_snapshot_rids.size()) + " orphaned spatial snapshots"); // Don't have useful data to report here so just report count.
+		for (const RID &rid : spatial_snapshot_rids) {
+			free_spatial_snapshot(rid);
+		}
+	}
+
+	// Clean up all remaining spatial context RIDs.
+	LocalVector<RID> spatial_context_rids = spatial_context_owner.get_owned_list();
+	if (!spatial_context_rids.is_empty()) {
+		print_verbose("OpenXR: Found " + String::num_int64(spatial_context_rids.size()) + " orphaned spatial contexts"); // Don't have useful data to report here so just report count.
+		for (const RID &rid : spatial_context_rids) {
+			free_spatial_context(rid);
+		}
+	}
+}
+
+bool OpenXRSpatialEntityExtension::get_active() const {
+	return spatial_entity_ext;
+}
+
+bool OpenXRSpatialEntityExtension::_load_capabilities() {
+	if (capabilities_load_state == 0) {
+		if (!spatial_entity_ext) {
+			return false;
+		}
+
+		OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+		ERR_FAIL_NULL_V(openxr_api, false);
+
+		XrInstance instance = openxr_api->get_instance();
+		ERR_FAIL_COND_V(instance == XR_NULL_HANDLE, false);
+		XrSystemId system_id = openxr_api->get_system_id();
+		ERR_FAIL_COND_V(system_id == 0, false);
+
+		// If we fail before this point, this may be called too early.
+		// Assume failure so we don't keep trying this unless we succeed.
+		capabilities_load_state = 2;
+
+		// Check our capabilities.
+		Vector<XrSpatialCapabilityEXT> capabilities;
+		uint32_t capability_size = 0;
+		XrResult result = xrEnumerateSpatialCapabilitiesEXT(instance, system_id, 0, &capability_size, nullptr);
+		if (XR_FAILED(result)) {
+			// Not successful? then exit.
+			ERR_FAIL_V_MSG(false, "OpenXR: Failed to get spatial entity capability count [" + openxr_api->get_error_string(result) + "]");
+		}
+
+		if (capability_size > 0) {
+			capabilities.resize(capability_size);
+			result = xrEnumerateSpatialCapabilitiesEXT(instance, system_id, capabilities.size(), &capability_size, capabilities.ptrw());
+			if (XR_FAILED(result)) {
+				// Not successful? then exit.
+				ERR_FAIL_V_MSG(false, "OpenXR: Failed to get spatial entity capabilities [" + openxr_api->get_error_string(result) + "]");
+			}
+
+			// Loop through capabilities
+			for (const XrSpatialCapabilityEXT &capability : capabilities) {
+				print_verbose("OpenXR: Found spatial entity capability " + get_spatial_capability_name(capability) + ".");
+
+				SpatialEntityCapabality &spatial_entity_capability = supported_capabilities[capability];
+
+				// retrieve component types for this capability
+				XrSpatialCapabilityComponentTypesEXT component_types = {
+					XR_TYPE_SPATIAL_CAPABILITY_COMPONENT_TYPES_EXT, // type
+					nullptr, // next
+					0, // componentTypeCapacityInput
+					0, // componentTypeCountOutput
+					nullptr // componentTypes
+				};
+				result = xrEnumerateSpatialCapabilityComponentTypesEXT(instance, system_id, capability, &component_types);
+				if (XR_FAILED(result)) {
+					// Not successful? just keep going.
+					ERR_PRINT("OpenXR: Failed to get spatial entity component type count [" + openxr_api->get_error_string(result) + "]");
+				} else if (component_types.componentTypeCountOutput > 0) {
+					spatial_entity_capability.component_types.resize(component_types.componentTypeCountOutput);
+					component_types.componentTypeCapacityInput = spatial_entity_capability.component_types.size();
+					component_types.componentTypeCountOutput = 0;
+					component_types.componentTypes = spatial_entity_capability.component_types.ptrw();
+					result = xrEnumerateSpatialCapabilityComponentTypesEXT(instance, system_id, capability, &component_types);
+					if (XR_FAILED(result)) {
+						// Not successful? just keep going.
+						ERR_PRINT("OpenXR: Failed to get spatial entity component types [" + openxr_api->get_error_string(result) + "]");
+					} else if (is_print_verbose_enabled()) {
+						for (const XrSpatialComponentTypeEXT &component_type : spatial_entity_capability.component_types) {
+							print_verbose("- component type " + get_spatial_component_type_name(component_type));
+						}
+					}
+				}
+
+				// Retrieve features for this capability
+				result = xrEnumerateSpatialCapabilityFeaturesEXT(instance, system_id, capability, 0, &capability_size, nullptr);
+				if (XR_FAILED(result)) {
+					// Not successful? just keep going.
+					ERR_PRINT("OpenXR: Failed to get spatial entity feature count [" + openxr_api->get_error_string(result) + "]");
+				} else if (capability_size > 0) {
+					spatial_entity_capability.features.resize(capability_size);
+					result = xrEnumerateSpatialCapabilityFeaturesEXT(instance, system_id, capability, spatial_entity_capability.features.size(), &capability_size, spatial_entity_capability.features.ptrw());
+					if (XR_FAILED(result)) {
+						// Not successful? just keep going.
+						ERR_PRINT("OpenXR: Failed to get spatial entity features [" + openxr_api->get_error_string(result) + "]");
+					} else if (is_print_verbose_enabled()) {
+						for (const XrSpatialCapabilityFeatureEXT &feature : spatial_entity_capability.features) {
+							print_verbose("- feature " + get_spatial_feature_name(feature));
+						}
+					}
+				}
+			}
+		}
+
+		capabilities_load_state = 1; // success!
+	}
+
+	return capabilities_load_state == 1;
+}
+
+bool OpenXRSpatialEntityExtension::supports_capability(XrSpatialCapabilityEXT p_capability) {
+	if (!_load_capabilities()) {
+		return false;
+	}
+
+	return supported_capabilities.has(p_capability);
+}
+
+bool OpenXRSpatialEntityExtension::_supports_capability(Capability p_capability) {
+	return supports_capability((XrSpatialCapabilityEXT)p_capability);
+}
+
+bool OpenXRSpatialEntityExtension::supports_component_type(XrSpatialCapabilityEXT p_capability, XrSpatialComponentTypeEXT p_component_type) {
+	if (!_load_capabilities()) {
+		return false;
+	}
+
+	if (supported_capabilities.has(p_capability)) {
+		return supported_capabilities[p_capability].component_types.has(p_component_type);
+	}
+	return false;
+}
+
+bool OpenXRSpatialEntityExtension::_supports_component_type(Capability p_capability, ComponentType p_component_type) {
+	return supports_component_type((XrSpatialCapabilityEXT)p_capability, (XrSpatialComponentTypeEXT)p_component_type);
+}
+
+bool OpenXRSpatialEntityExtension::on_event_polled(const XrEventDataBuffer &event) {
+	if (!get_active()) {
+		return false;
+	}
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, false);
+
+	switch (event.type) {
+		case XR_TYPE_EVENT_DATA_SPATIAL_DISCOVERY_RECOMMENDED_EXT: {
+			const XrEventDataSpatialDiscoveryRecommendedEXT *eventdata = (const XrEventDataSpatialDiscoveryRecommendedEXT *)&event;
+
+			// TODO: Should maybe keep a HashMap for a reverse lookup.
+
+			LocalVector<RID> spatial_context_rids = spatial_context_owner.get_owned_list();
+			for (const RID &rid : spatial_context_rids) {
+				if (get_spatial_context_handle(rid) == eventdata->spatialContext) {
+					emit_signal(SNAME("spatial_discovery_recommended"), rid);
+				}
+			}
+
+			return true;
+		} break;
+		default: {
+			return false;
+		} break;
+	}
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Spatial contexts
+
+Ref<OpenXRFutureResult> OpenXRSpatialEntityExtension::create_spatial_context(const TypedArray<OpenXRSpatialCapabilityConfigurationBaseHeader> &p_capability_configurations, Ref<OpenXRStructureBase> p_next, const Callable &p_user_callback) {
+	if (!get_active()) {
+		return nullptr;
+	}
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, nullptr);
+
+	OpenXRFutureExtension *future_api = OpenXRFutureExtension::get_singleton();
+	ERR_FAIL_NULL_V(future_api, nullptr);
+
+	// Parse our configuration.
+	Vector<XrSpatialCapabilityConfigurationBaseHeaderEXT *> configuration;
+	for (Ref<OpenXRSpatialCapabilityConfigurationBaseHeader> capability_configuration : p_capability_configurations) {
+		ERR_FAIL_COND_V(capability_configuration.is_null(), nullptr);
+
+		XrSpatialCapabilityConfigurationBaseHeaderEXT *config = capability_configuration->get_configuration();
+		if (config != nullptr) {
+			configuration.push_back(config);
+		}
+	}
+
+	void *next = nullptr;
+	if (p_next.is_valid()) {
+		next = p_next->get_header(next);
+	}
+
+	XrSpatialContextCreateInfoEXT create_info = {
+		XR_TYPE_SPATIAL_CONTEXT_CREATE_INFO_EXT, // type
+		next, // next
+		uint32_t(configuration.size()), // capabilityConfigCount
+		configuration.is_empty() ? nullptr : configuration.ptr(), // capabilityConfigs
+	};
+	XrFutureEXT future = XR_NULL_HANDLE;
+	XrResult xr_result = xrCreateSpatialContextAsyncEXT(openxr_api->get_session(), &create_info, &future);
+	if (XR_FAILED(xr_result)) {
+		// Not successful? then exit.
+		ERR_FAIL_V_MSG(Ref<OpenXRFutureResult>(), "OpenXR: Failed to create spatial context [" + openxr_api->get_error_string(xr_result) + "]");
+	}
+
+	// Create our future result
+	Ref<OpenXRFutureResult> future_result = future_api->register_future(future, callable_mp(this, &OpenXRSpatialEntityExtension::_on_context_creation_ready).bind(p_user_callback));
+
+	return future_result;
+}
+
+void OpenXRSpatialEntityExtension::_on_context_creation_ready(Ref<OpenXRFutureResult> p_future_result, const Callable &p_user_callback) {
+	// Complete context creation...
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+
+	XrCreateSpatialContextCompletionEXT completion = {
+		XR_TYPE_CREATE_SPATIAL_CONTEXT_COMPLETION_EXT, // type
+		nullptr, // next
+		XR_RESULT_MAX_ENUM, // futureResult
+		XR_NULL_HANDLE // spatialContext
+	};
+	XrResult result = xrCreateSpatialContextCompleteEXT(openxr_api->get_session(), p_future_result->get_future(), &completion);
+	if (XR_FAILED(result)) { // Did our xrCreateSpatialContextCompleteEXT call fail?
+		// Log issue and fail.
+		ERR_FAIL_MSG("OpenXR: Failed to complete spatial context create future [" + openxr_api->get_error_string(result) + "]");
+	}
+	if (XR_FAILED(completion.futureResult)) { // Did our completion fail?
+		// Log issue and fail.
+		ERR_FAIL_MSG("OpenXR: Failed to complete spatial context creation [" + openxr_api->get_error_string(completion.futureResult) + "]");
+	}
+
+	// Wrap our spatial context
+	SpatialContextData spatial_context_data;
+	spatial_context_data.spatial_context = completion.spatialContext;
+
+	// Store this as an RID so we keep track of it.
+	RID context_rid = spatial_context_owner.make_rid(spatial_context_data);
+
+	// Set our RID as our result value on our future.
+	p_future_result->set_result_value(context_rid);
+
+	// And perform our callback if we have one.
+	if (p_user_callback.is_valid()) {
+		p_user_callback.call(context_rid);
+	}
+}
+
+bool OpenXRSpatialEntityExtension::get_spatial_context_ready(RID p_spatial_context) const {
+	SpatialContextData *context_data = spatial_context_owner.get_or_null(p_spatial_context);
+	ERR_FAIL_NULL_V(context_data, false);
+
+	return context_data->spatial_context != XR_NULL_HANDLE;
+}
+
+void OpenXRSpatialEntityExtension::free_spatial_context(RID p_spatial_context) {
+	SpatialContextData *context_data = spatial_context_owner.get_or_null(p_spatial_context);
+	ERR_FAIL_NULL(context_data);
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+
+	if (context_data->spatial_context != XR_NULL_HANDLE) {
+		// Destroy our spatial context
+		XrResult result = xrDestroySpatialContextEXT(context_data->spatial_context);
+		if (XR_FAILED(result)) {
+			WARN_PRINT("OpenXR: Failed to destroy the spatial context [" + openxr_api->get_error_string(result) + "]");
+		}
+		context_data->spatial_context = XR_NULL_HANDLE;
+
+		// And remove our RID.
+		spatial_context_owner.free(p_spatial_context);
+	}
+}
+
+XrSpatialContextEXT OpenXRSpatialEntityExtension::get_spatial_context_handle(RID p_spatial_context) const {
+	SpatialContextData *context_data = spatial_context_owner.get_or_null(p_spatial_context);
+	ERR_FAIL_NULL_V(context_data, XR_NULL_HANDLE);
+
+	return context_data->spatial_context;
+}
+
+// For exposing this to GDExtension
+uint64_t OpenXRSpatialEntityExtension::_get_spatial_context_handle(RID p_spatial_context) const {
+	return (uint64_t)get_spatial_context_handle(p_spatial_context);
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Discovery queries
+
+Ref<OpenXRFutureResult> OpenXRSpatialEntityExtension::discover_spatial_entities(RID p_spatial_context, const Vector<XrSpatialComponentTypeEXT> &p_component_types, Ref<OpenXRStructureBase> p_next, const Callable &p_user_callback) {
+	if (!get_active()) {
+		return nullptr;
+	}
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, nullptr);
+
+	OpenXRFutureExtension *future_api = OpenXRFutureExtension::get_singleton();
+	ERR_FAIL_NULL_V(future_api, nullptr);
+
+	void *next = nullptr;
+	if (p_next.is_valid()) {
+		next = p_next->get_header(next);
+	}
+
+	// Start our discovery snapshot.
+	XrSpatialDiscoverySnapshotCreateInfoEXT create_info = {
+		XR_TYPE_SPATIAL_DISCOVERY_SNAPSHOT_CREATE_INFO_EXT, // type
+		next, // next
+		(uint32_t)p_component_types.size(), // componentTypeCount
+		p_component_types.is_empty() ? nullptr : p_component_types.ptr() // componentTypes
+	};
+
+	XrFutureEXT future;
+	XrResult result = xrCreateSpatialDiscoverySnapshotAsyncEXT(get_spatial_context_handle(p_spatial_context), &create_info, &future);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(nullptr, "OpenXR: Failed to initiate snapshot discovery [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	// Create our future result
+	Ref<OpenXRFutureResult> future_result = future_api->register_future(future, callable_mp(this, &OpenXRSpatialEntityExtension::_on_discovered_spatial_entities).bind(p_spatial_context, p_user_callback));
+
+	return future_result;
+}
+
+// For calls from GDExtension
+Ref<OpenXRFutureResult> OpenXRSpatialEntityExtension::_discover_spatial_entities(RID p_spatial_context, const PackedInt64Array &p_component_types, Ref<OpenXRStructureBase> p_next, const Callable &p_callback) {
+	Vector<XrSpatialComponentTypeEXT> component_types;
+	component_types.resize(p_component_types.size());
+	XrSpatialComponentTypeEXT *ptr = component_types.ptrw();
+	for (const int64_t &component_type : p_component_types) {
+		*ptr = (XrSpatialComponentTypeEXT)component_type;
+		ptr++;
+	}
+
+	return discover_spatial_entities(p_spatial_context, component_types, p_next, p_callback);
+}
+
+void OpenXRSpatialEntityExtension::_on_discovered_spatial_entities(Ref<OpenXRFutureResult> p_future_result, RID p_discovery_spatial_context, const Callable &p_user_callback) {
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+
+	XrSpatialContextEXT xr_spatial_context = get_spatial_context_handle(p_discovery_spatial_context);
+	ERR_FAIL_COND(xr_spatial_context == XR_NULL_HANDLE);
+
+	XrCreateSpatialDiscoverySnapshotCompletionInfoEXT completion_info = {
+		XR_TYPE_CREATE_SPATIAL_DISCOVERY_SNAPSHOT_COMPLETION_INFO_EXT, // type
+		nullptr, // next
+		openxr_api->get_play_space(), // baseSpace
+		openxr_api->get_predicted_display_time(), // time
+		p_future_result->get_future() // future
+	};
+
+	XrCreateSpatialDiscoverySnapshotCompletionEXT completion = {
+		XR_TYPE_CREATE_SPATIAL_DISCOVERY_SNAPSHOT_COMPLETION_EXT, // type
+		nullptr, // next
+		XR_SUCCESS, // futureResult
+		XR_NULL_HANDLE // snapshot
+	};
+	XrResult result = xrCreateSpatialDiscoverySnapshotCompleteEXT(xr_spatial_context, &completion_info, &completion);
+
+	if (XR_FAILED(result)) { // Did our xrCreateSpatialContextCompleteEXT call fail?
+		// And log issue.
+		ERR_FAIL_MSG("OpenXR: Failed to complete discovery query future [" + openxr_api->get_error_string(result) + "]");
+	}
+	if (XR_FAILED(completion.futureResult)) { // Did our completion fail?
+		// And log issue.
+		ERR_FAIL_MSG("OpenXR: Failed to complete discovery query [" + openxr_api->get_error_string(completion.futureResult) + "]");
+	}
+
+	// Wrap our spatial snapshot
+	SpatialSnapshotData snapshot_data;
+	snapshot_data.spatial_context = p_discovery_spatial_context;
+	snapshot_data.spatial_snapshot = completion.snapshot;
+
+	// Store this as an RID so we keep track of it.
+	RID snapshot_rid = spatial_snapshot_owner.make_rid(snapshot_data);
+
+	// Set our RID as our result value on our future.
+	p_future_result->set_result_value(snapshot_rid);
+
+	// And perform our callback if we have one.
+	if (p_user_callback.is_valid()) {
+		p_user_callback.call(snapshot_rid);
+	}
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Update query
+
+RID OpenXRSpatialEntityExtension::update_spatial_entities(RID p_spatial_context, const LocalVector<RID> &p_entities, const LocalVector<XrSpatialComponentTypeEXT> &p_component_types, Ref<OpenXRStructureBase> p_next) {
+	if (!get_active()) {
+		return RID();
+	}
+
+	ERR_FAIL_COND_V(p_entities.is_empty(), RID());
+
+	SpatialContextData *context_data = spatial_context_owner.get_or_null(p_spatial_context);
+	ERR_FAIL_NULL_V(context_data, RID());
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, RID());
+
+	// Convert our entity RIDs to XrSpatialEntityEXT
+	thread_local LocalVector<XrSpatialEntityEXT> entities;
+
+	entities.resize(p_entities.size());
+	XrSpatialEntityEXT *ptr = entities.ptr();
+	for (const RID &rid : p_entities) {
+		SpatialEntityData *entity_data = spatial_entity_owner.get_or_null(rid);
+		*ptr = entity_data ? entity_data->entity : XR_NULL_HANDLE;
+		ptr++;
+	}
+
+	void *next = nullptr;
+	if (p_next.is_valid()) {
+		next = p_next->get_header(next);
+	}
+
+	SpatialSnapshotData spatial_snapshot_data;
+
+	// Store the context we used for this discovery query
+	spatial_snapshot_data.spatial_context = p_spatial_context;
+
+	// Do update
+	XrSpatialUpdateSnapshotCreateInfoEXT create_info = {
+		XR_TYPE_SPATIAL_UPDATE_SNAPSHOT_CREATE_INFO_EXT, // type
+		next, // next
+		(uint32_t)entities.size(), // entityCount,
+		entities.ptr(), // entities
+		(uint32_t)p_component_types.size(), // componentTypeCount
+		p_component_types.is_empty() ? nullptr : p_component_types.ptr(), // componentTypes
+		openxr_api->get_play_space(), // baseSpace
+		openxr_api->get_predicted_display_time() // time
+	};
+	XrResult result = xrCreateSpatialUpdateSnapshotEXT(context_data->spatial_context, &create_info, &spatial_snapshot_data.spatial_snapshot);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(RID(), "OpenXR: Failed to create update snapshot [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	// Store our snapshot in an RID and return.
+	return spatial_snapshot_owner.make_rid(spatial_snapshot_data);
+}
+
+RID OpenXRSpatialEntityExtension::_update_spatial_entities(RID p_spatial_context, const TypedArray<RID> &p_entities, const PackedInt64Array &p_component_types, Ref<OpenXRStructureBase> p_next) {
+	thread_local LocalVector<RID> entities;
+	entities.resize(p_entities.size());
+	RID *rids = entities.ptr();
+	for (const RID rid : p_entities) {
+		*rids = rid;
+		rids++;
+	}
+
+	thread_local LocalVector<XrSpatialComponentTypeEXT> component_types;
+	component_types.resize(p_component_types.size());
+	XrSpatialComponentTypeEXT *ptr = component_types.ptr();
+	for (const int64_t &component_type : p_component_types) {
+		*ptr = (XrSpatialComponentTypeEXT)component_type;
+		ptr++;
+	}
+
+	return update_spatial_entities(p_spatial_context, entities, component_types, p_next);
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Snapshot data
+
+void OpenXRSpatialEntityExtension::free_spatial_snapshot(RID p_spatial_snapshot) {
+	SpatialSnapshotData *snapshot_data = spatial_snapshot_owner.get_or_null(p_spatial_snapshot);
+	ERR_FAIL_NULL(snapshot_data);
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+
+	if (snapshot_data->spatial_snapshot != XR_NULL_HANDLE) {
+		// Destroy our spatial context
+		XrResult result = xrDestroySpatialSnapshotEXT(snapshot_data->spatial_snapshot);
+		if (XR_FAILED(result)) {
+			WARN_PRINT("OpenXR: Failed to destroy the spatial snapshot [" + openxr_api->get_error_string(result) + "]");
+		}
+		snapshot_data->spatial_snapshot = XR_NULL_HANDLE;
+	}
+
+	// And remove our RID.
+	spatial_snapshot_owner.free(p_spatial_snapshot);
+}
+
+XrSpatialSnapshotEXT OpenXRSpatialEntityExtension::get_spatial_snapshot_handle(RID p_spatial_snapshot) const {
+	SpatialSnapshotData *snapshot_data = spatial_snapshot_owner.get_or_null(p_spatial_snapshot);
+	ERR_FAIL_NULL_V(snapshot_data, XR_NULL_HANDLE);
+
+	return snapshot_data->spatial_snapshot;
+}
+
+RID OpenXRSpatialEntityExtension::get_spatial_snapshot_context(RID p_spatial_snapshot) const {
+	SpatialSnapshotData *snapshot_data = spatial_snapshot_owner.get_or_null(p_spatial_snapshot);
+	ERR_FAIL_NULL_V(snapshot_data, RID());
+
+	return snapshot_data->spatial_context;
+}
+
+// For exposing this to GDExtension
+uint64_t OpenXRSpatialEntityExtension::_get_spatial_snapshot_handle(RID p_spatial_snapshot) const {
+	return (uint64_t)get_spatial_snapshot_handle(p_spatial_snapshot);
+}
+
+bool OpenXRSpatialEntityExtension::query_snapshot(RID p_spatial_snapshot, const TypedArray<OpenXRSpatialComponentData> &p_component_data, Ref<OpenXRStructureBase> p_next) {
+	SpatialSnapshotData *snapshot_data = spatial_snapshot_owner.get_or_null(p_spatial_snapshot);
+	ERR_FAIL_NULL_V(snapshot_data, false);
+
+	ERR_FAIL_COND_V(p_component_data.is_empty(), false);
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, false);
+
+	Ref<OpenXRSpatialQueryResultData> query_result_data = p_component_data[0];
+	ERR_FAIL_COND_V_MSG(query_result_data.is_null(), false, "OpenXR: The first component must be of type OpenXRSpatialQueryResultData");
+
+	// Gather component types we need to query.
+	Vector<XrSpatialComponentTypeEXT> component_types;
+	for (Ref<OpenXRSpatialComponentData> component_data : p_component_data) {
+		if (component_data.is_valid()) {
+			XrSpatialComponentTypeEXT component_type = component_data->get_component_type();
+			if (component_type != XR_SPATIAL_COMPONENT_TYPE_MAX_ENUM_EXT) {
+				component_types.push_back(component_type);
+			}
+		}
+	}
+
+	void *next = nullptr;
+	if (p_next.is_valid()) {
+		next = p_next->get_header(next);
+	}
+
+	XrSpatialComponentDataQueryConditionEXT query_condition = {
+		XR_TYPE_SPATIAL_COMPONENT_DATA_QUERY_CONDITION_EXT, // type
+		next, // next
+		0, // componentTypeCount
+		nullptr // componentTypes
+	};
+
+	query_condition.componentTypeCount = component_types.size();
+	query_condition.componentTypes = component_types.ptr();
+
+	XrSpatialComponentDataQueryResultEXT *query_result = (XrSpatialComponentDataQueryResultEXT *)query_result_data->get_structure_data(nullptr);
+	XrResult result = xrQuerySpatialComponentDataEXT(snapshot_data->spatial_snapshot, &query_condition, query_result);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(false, "OpenXR: Failed to query snapshot count [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	// Nothing to do?
+	if (query_result->entityIdCountOutput == 0) {
+		return true;
+	}
+
+	// This indicates an issue in the XR runtime, we should have a state for every entity so these counts must match.
+	ERR_FAIL_COND_V_MSG(query_result->entityIdCountOutput != query_result->entityStateCountOutput, false, "OpenXR: Entity ID count and entity state count don't match!");
+
+	// Allocate our memory and parse our next structure
+	next = nullptr;
+	for (Ref<OpenXRSpatialComponentData> component_data : p_component_data) {
+		if (component_data.is_valid()) {
+			component_data->set_capacity(query_result->entityIdCountOutput);
+			XrSpatialComponentTypeEXT component_type = component_data->get_component_type();
+			if (component_type != XR_SPATIAL_COMPONENT_TYPE_MAX_ENUM_EXT) {
+				next = component_data->get_structure_data(next);
+			}
+		}
+	}
+
+	query_result = (XrSpatialComponentDataQueryResultEXT *)query_result_data->get_structure_data(next);
+	result = xrQuerySpatialComponentDataEXT(snapshot_data->spatial_snapshot, &query_condition, query_result);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(false, "OpenXR: Failed to query snapshot data [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	return true;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Buffers from snapshot
+
+String OpenXRSpatialEntityExtension::get_string(RID p_spatial_snapshot, XrSpatialBufferIdEXT p_buffer_id) const {
+	String ret;
+
+	SpatialSnapshotData *snapshot_data = spatial_snapshot_owner.get_or_null(p_spatial_snapshot);
+	ERR_FAIL_NULL_V(snapshot_data, ret);
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, ret);
+
+	XrSpatialBufferGetInfoEXT info = {
+		XR_TYPE_SPATIAL_BUFFER_GET_INFO_EXT, // type
+		nullptr, // next
+		p_buffer_id, // bufferId
+	};
+
+	uint32_t count = 0;
+	XrResult result = xrGetSpatialBufferStringEXT(snapshot_data->spatial_snapshot, &info, 0, &count, nullptr);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(ret, "OpenXR: Failed to get buffer size [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	LocalVector<char> buffer;
+	buffer.resize(count + 1);
+	buffer[count] = '\0'; // + 1 and setting a zero terminator just in case runtime is not including this.
+
+	result = xrGetSpatialBufferStringEXT(snapshot_data->spatial_snapshot, &info, buffer.size(), &count, buffer.ptr());
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(ret, "OpenXR: Failed to get buffer [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	ret = String::utf8(buffer.ptr());
+	return ret;
+}
+
+PackedByteArray OpenXRSpatialEntityExtension::get_uint8_buffer(RID p_spatial_snapshot, XrSpatialBufferIdEXT p_buffer_id) const {
+	PackedByteArray ret;
+
+	SpatialSnapshotData *snapshot_data = spatial_snapshot_owner.get_or_null(p_spatial_snapshot);
+	ERR_FAIL_NULL_V(snapshot_data, ret);
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, ret);
+
+	XrSpatialBufferGetInfoEXT info = {
+		XR_TYPE_SPATIAL_BUFFER_GET_INFO_EXT, // type
+		nullptr, // next
+		p_buffer_id, // bufferId
+	};
+
+	uint32_t count = 0;
+	XrResult result = xrGetSpatialBufferUint8EXT(snapshot_data->spatial_snapshot, &info, 0, &count, nullptr);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(ret, "OpenXR: Failed to get buffer size [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	ret.resize(count);
+
+	result = xrGetSpatialBufferUint8EXT(snapshot_data->spatial_snapshot, &info, ret.size(), &count, (uint8_t *)ret.ptrw());
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(PackedByteArray(), "OpenXR: Failed to get buffer [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	return ret;
+}
+
+Vector<uint16_t> OpenXRSpatialEntityExtension::get_uint16_buffer(RID p_spatial_snapshot, XrSpatialBufferIdEXT p_buffer_id) const {
+	Vector<uint16_t> ret;
+
+	SpatialSnapshotData *snapshot_data = spatial_snapshot_owner.get_or_null(p_spatial_snapshot);
+	ERR_FAIL_NULL_V(snapshot_data, ret);
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, ret);
+
+	XrSpatialBufferGetInfoEXT info = {
+		XR_TYPE_SPATIAL_BUFFER_GET_INFO_EXT, // type
+		nullptr, // next
+		p_buffer_id, // bufferId
+	};
+
+	uint32_t count = 0;
+	XrResult result = xrGetSpatialBufferUint16EXT(snapshot_data->spatial_snapshot, &info, 0, &count, nullptr);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(ret, "OpenXR: Failed to get buffer size [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	ret.resize(count);
+
+	result = xrGetSpatialBufferUint16EXT(snapshot_data->spatial_snapshot, &info, ret.size(), &count, ret.ptrw());
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(Vector<uint16_t>(), "OpenXR: Failed to get buffer [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	return ret;
+}
+
+Vector<uint32_t> OpenXRSpatialEntityExtension::get_uint32_buffer(RID p_spatial_snapshot, XrSpatialBufferIdEXT p_buffer_id) const {
+	Vector<uint32_t> ret;
+
+	SpatialSnapshotData *snapshot_data = spatial_snapshot_owner.get_or_null(p_spatial_snapshot);
+	ERR_FAIL_NULL_V(snapshot_data, ret);
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, ret);
+
+	XrSpatialBufferGetInfoEXT info = {
+		XR_TYPE_SPATIAL_BUFFER_GET_INFO_EXT, // type
+		nullptr, // next
+		p_buffer_id, // bufferId
+	};
+
+	uint32_t count = 0;
+	XrResult result = xrGetSpatialBufferUint32EXT(snapshot_data->spatial_snapshot, &info, 0, &count, nullptr);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(ret, "OpenXR: Failed to get buffer size [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	ret.resize(count);
+
+	result = xrGetSpatialBufferUint32EXT(snapshot_data->spatial_snapshot, &info, ret.size(), &count, ret.ptrw());
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(Vector<uint32_t>(), "OpenXR: Failed to get buffer [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	return ret;
+}
+
+PackedFloat32Array OpenXRSpatialEntityExtension::get_float_buffer(RID p_spatial_snapshot, XrSpatialBufferIdEXT p_buffer_id) const {
+	PackedFloat32Array ret;
+
+	SpatialSnapshotData *snapshot_data = spatial_snapshot_owner.get_or_null(p_spatial_snapshot);
+	ERR_FAIL_NULL_V(snapshot_data, ret);
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, ret);
+
+	XrSpatialBufferGetInfoEXT info = {
+		XR_TYPE_SPATIAL_BUFFER_GET_INFO_EXT, // type
+		nullptr, // next
+		p_buffer_id, // bufferId
+	};
+
+	uint32_t count = 0;
+	XrResult result = xrGetSpatialBufferFloatEXT(snapshot_data->spatial_snapshot, &info, 0, &count, nullptr);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(ret, "OpenXR: Failed to get buffer size [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	ret.resize(count);
+
+	result = xrGetSpatialBufferFloatEXT(snapshot_data->spatial_snapshot, &info, ret.size(), &count, ret.ptrw());
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(PackedFloat32Array(), "OpenXR: Failed to get buffer [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	return ret;
+}
+
+PackedVector2Array OpenXRSpatialEntityExtension::get_vector2_buffer(RID p_spatial_snapshot, XrSpatialBufferIdEXT p_buffer_id) const {
+	PackedVector2Array ret;
+
+	SpatialSnapshotData *snapshot_data = spatial_snapshot_owner.get_or_null(p_spatial_snapshot);
+	ERR_FAIL_NULL_V(snapshot_data, ret);
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, ret);
+
+	XrSpatialBufferGetInfoEXT info = {
+		XR_TYPE_SPATIAL_BUFFER_GET_INFO_EXT, // type
+		nullptr, // next
+		p_buffer_id, // bufferId
+	};
+
+	uint32_t count = 0;
+	XrResult result = xrGetSpatialBufferVector2fEXT(snapshot_data->spatial_snapshot, &info, 0, &count, nullptr);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(ret, "OpenXR: Failed to get buffer size [" + openxr_api->get_error_string(result) + "]");
+	}
+
+#ifdef REAL_T_IS_DOUBLE
+	// OpenXR XrVector2f is using floats, Godot Vector2 is using double, so we need to do a copy.
+	LocalVector<XrVector2f> buffer;
+	buffer.resize(count);
+
+	result = xrGetSpatialBufferVector2fEXT(snapshot_data->spatial_snapshot, &info, buffer.size(), &count, buffer.ptr());
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(ret, "OpenXR: Failed to get buffer [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	ret.resize(count);
+	Vector2 *ptr = ret.ptrw();
+	for (uint32_t i = 0; i < count; i++) {
+		ptr[i].x = buffer[i].x;
+		ptr[i].y = buffer[i].y;
+	}
+#else
+	// OpenXR's XrVector2f and Godots Vector2 should be interchangeable.
+	ret.resize(count);
+
+	result = xrGetSpatialBufferVector2fEXT(snapshot_data->spatial_snapshot, &info, ret.size(), &count, (XrVector2f *)ret.ptrw());
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(PackedVector2Array(), "OpenXR: Failed to get buffer [" + openxr_api->get_error_string(result) + "]");
+	}
+#endif
+
+	return ret;
+}
+
+PackedVector3Array OpenXRSpatialEntityExtension::get_vector3_buffer(RID p_spatial_snapshot, XrSpatialBufferIdEXT p_buffer_id) const {
+	PackedVector3Array ret;
+
+	SpatialSnapshotData *snapshot_data = spatial_snapshot_owner.get_or_null(p_spatial_snapshot);
+	ERR_FAIL_NULL_V(snapshot_data, ret);
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, ret);
+
+	XrSpatialBufferGetInfoEXT info = {
+		XR_TYPE_SPATIAL_BUFFER_GET_INFO_EXT, // type
+		nullptr, // next
+		p_buffer_id, // bufferId
+	};
+
+	uint32_t count = 0;
+	XrResult result = xrGetSpatialBufferVector3fEXT(snapshot_data->spatial_snapshot, &info, 0, &count, nullptr);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(ret, "OpenXR: Failed to get buffer size [" + openxr_api->get_error_string(result) + "]");
+	}
+
+#ifdef REAL_T_IS_DOUBLE
+	// OpenXR XrVector3f is using floats, Godot Vector3 is using double, so we need to do a copy.
+	LocalVector<XrVector3f> buffer;
+	buffer.resize(count);
+
+	result = xrGetSpatialBufferVector3fEXT(snapshot_data->spatial_snapshot, &info, buffer.size(), &count, buffer.ptr());
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(ret, "OpenXR: Failed to get buffer [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	ret.resize(count);
+	Vector3 *ptr = ret.ptrw();
+	for (uint32_t i = 0; i < count; i++) {
+		ptr[i].x = buffer[i].x;
+		ptr[i].y = buffer[i].y;
+		ptr[i].z = buffer[i].z;
+	}
+#else
+	// OpenXR's XrVector3f and Godots Vector3 should be interchangeable.
+	ret.resize(count);
+
+	result = xrGetSpatialBufferVector3fEXT(snapshot_data->spatial_snapshot, &info, ret.size(), &count, (XrVector3f *)ret.ptrw());
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(PackedVector3Array(), "OpenXR: Failed to get buffer [" + openxr_api->get_error_string(result) + "]");
+	}
+#endif
+
+	return ret;
+}
+
+String OpenXRSpatialEntityExtension::_get_string(RID p_spatial_snapshot, uint64_t p_buffer_id) const {
+	return get_string(p_spatial_snapshot, (XrSpatialBufferIdEXT)p_buffer_id);
+}
+
+PackedByteArray OpenXRSpatialEntityExtension::_get_uint8_buffer(RID p_spatial_snapshot, uint64_t p_buffer_id) const {
+	return get_uint8_buffer(p_spatial_snapshot, (XrSpatialBufferIdEXT)p_buffer_id);
+}
+
+PackedInt32Array OpenXRSpatialEntityExtension::_get_uint16_buffer(RID p_spatial_snapshot, uint64_t p_buffer_id) const {
+	PackedInt32Array ret;
+	Vector<uint16_t> buffer = get_uint16_buffer(p_spatial_snapshot, (XrSpatialBufferIdEXT)p_buffer_id);
+
+	if (!buffer.is_empty()) {
+		// We don't have PackedInt16Array so we convert to PackedInt32Array
+
+		ret.resize(buffer.size());
+
+		int size = ret.size();
+		int32_t *ptr = ret.ptrw();
+		for (int i = 0; i < size; i++) {
+			ptr[i] = buffer[i];
+		}
+	}
+
+	return ret;
+}
+
+PackedInt32Array OpenXRSpatialEntityExtension::_get_uint32_buffer(RID p_spatial_snapshot, uint64_t p_buffer_id) const {
+	PackedInt32Array ret;
+	Vector<uint32_t> buffer = get_uint32_buffer(p_spatial_snapshot, (XrSpatialBufferIdEXT)p_buffer_id);
+
+	if (!buffer.is_empty()) {
+		// Note, we don't have a UINT32 array that we can use with GDScript and using an INT64 array is overkill.
+		// Bit wasteful this but...
+
+		ret.resize(buffer.size());
+
+		int size = ret.size();
+		int32_t *ptr = ret.ptrw();
+		for (int i = 0; i < size; i++) {
+			ptr[i] = buffer[i];
+		}
+	}
+
+	return ret;
+}
+
+PackedFloat32Array OpenXRSpatialEntityExtension::_get_float_buffer(RID p_spatial_snapshot, uint64_t p_buffer_id) const {
+	return get_float_buffer(p_spatial_snapshot, (XrSpatialBufferIdEXT)p_buffer_id);
+}
+
+PackedVector2Array OpenXRSpatialEntityExtension::_get_vector2_buffer(RID p_spatial_snapshot, uint64_t p_buffer_id) const {
+	return get_vector2_buffer(p_spatial_snapshot, (XrSpatialBufferIdEXT)p_buffer_id);
+}
+
+PackedVector3Array OpenXRSpatialEntityExtension::_get_vector3_buffer(RID p_spatial_snapshot, uint64_t p_buffer_id) const {
+	return get_vector3_buffer(p_spatial_snapshot, (XrSpatialBufferIdEXT)p_buffer_id);
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Entities
+
+RID OpenXRSpatialEntityExtension::find_spatial_entity(XrSpatialEntityIdEXT p_entity_id) const {
+	ERR_FAIL_COND_V(!get_active(), RID());
+
+	LocalVector<RID> entities = spatial_entity_owner.get_owned_list();
+	for (const RID &entity : entities) {
+		SpatialEntityData *entity_data = spatial_entity_owner.get_or_null(entity);
+		ERR_FAIL_NULL_V(entity_data, RID());
+
+		if (entity_data->entity_id == p_entity_id) {
+			return entity;
+		}
+	}
+
+	return RID();
+}
+
+RID OpenXRSpatialEntityExtension::_find_entity(uint64_t p_entity_id) {
+	return find_spatial_entity((XrSpatialEntityIdEXT)p_entity_id);
+}
+
+RID OpenXRSpatialEntityExtension::add_spatial_entity(RID p_spatial_context, XrSpatialEntityIdEXT p_entity_id, XrSpatialEntityEXT p_entity) {
+	ERR_FAIL_COND_V(!get_active(), RID());
+
+	// Entity has been created elsewhere, we just register it
+	SpatialEntityData spatial_entity_data;
+
+	spatial_entity_data.spatial_context = p_spatial_context;
+	spatial_entity_data.entity_id = p_entity_id;
+	spatial_entity_data.entity = p_entity;
+
+	return spatial_entity_owner.make_rid(spatial_entity_data);
+}
+
+RID OpenXRSpatialEntityExtension::_add_entity(RID p_spatial_context, uint64_t p_entity_id, uint64_t p_entity) {
+	return add_spatial_entity(p_spatial_context, (XrSpatialEntityIdEXT)p_entity_id, (XrSpatialEntityEXT)p_entity);
+}
+
+RID OpenXRSpatialEntityExtension::make_spatial_entity(RID p_spatial_context, XrSpatialEntityIdEXT p_entity_id) {
+	ERR_FAIL_COND_V(!get_active(), RID());
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, RID());
+
+	SpatialEntityData spatial_entity_data;
+
+	spatial_entity_data.spatial_context = p_spatial_context;
+	spatial_entity_data.entity_id = p_entity_id;
+	XrSpatialEntityFromIdCreateInfoEXT create_info = {
+		XR_TYPE_SPATIAL_ENTITY_FROM_ID_CREATE_INFO_EXT, // type
+		nullptr, // next
+		p_entity_id //entityId
+	};
+	XrResult result = xrCreateSpatialEntityFromIdEXT(get_spatial_context_handle(p_spatial_context), &create_info, &spatial_entity_data.entity);
+	if (XR_FAILED(result)) {
+		ERR_FAIL_V_MSG(RID(), "OpenXR: Failed to create spatial entity [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	return spatial_entity_owner.make_rid(spatial_entity_data);
+}
+
+RID OpenXRSpatialEntityExtension::_make_entity(RID p_spatial_context, uint64_t p_entity_id) {
+	return make_spatial_entity(p_spatial_context, (XrSpatialEntityIdEXT)p_entity_id);
+}
+
+XrSpatialEntityIdEXT OpenXRSpatialEntityExtension::get_spatial_entity_id(RID p_entity) const {
+	SpatialEntityData *entity_data = spatial_entity_owner.get_or_null(p_entity);
+	ERR_FAIL_NULL_V(entity_data, XR_NULL_ENTITY);
+
+	return entity_data->entity_id;
+}
+
+uint64_t OpenXRSpatialEntityExtension::_get_entity_id(RID p_entity) const {
+	return (uint64_t)get_spatial_entity_id(p_entity);
+}
+
+RID OpenXRSpatialEntityExtension::get_spatial_entity_context(RID p_entity) const {
+	SpatialEntityData *entity_data = spatial_entity_owner.get_or_null(p_entity);
+	ERR_FAIL_NULL_V(entity_data, RID());
+
+	return entity_data->spatial_context;
+}
+
+void OpenXRSpatialEntityExtension::free_spatial_entity(RID p_entity) {
+	SpatialEntityData *entity_data = spatial_entity_owner.get_or_null(p_entity);
+	ERR_FAIL_NULL(entity_data);
+	ERR_FAIL_COND(entity_data->entity == XR_NULL_HANDLE);
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+
+	XrResult result = xrDestroySpatialEntityEXT_ptr(entity_data->entity);
+	if (XR_FAILED(result)) {
+		WARN_PRINT("OpenXR: Failed to destroy spatial entity [" + openxr_api->get_error_string(result) + "]");
+	}
+
+	// And remove our RID.
+	spatial_entity_owner.free(p_entity);
+}
+
+String OpenXRSpatialEntityExtension::get_spatial_capability_name(XrSpatialCapabilityEXT p_capability){
+	XR_ENUM_SWITCH(XrSpatialCapabilityEXT, p_capability)
+}
+
+String OpenXRSpatialEntityExtension::get_spatial_component_type_name(XrSpatialComponentTypeEXT p_component_type){
+	XR_ENUM_SWITCH(XrSpatialComponentTypeEXT, p_component_type)
+}
+
+String OpenXRSpatialEntityExtension::get_spatial_feature_name(XrSpatialCapabilityFeatureEXT p_feature) {
+	XR_ENUM_SWITCH(XrSpatialCapabilityFeatureEXT, p_feature)
+}

+ 217 - 0
modules/openxr/extensions/spatial_entities/openxr_spatial_entity_extension.h

@@ -0,0 +1,217 @@
+/**************************************************************************/
+/*  openxr_spatial_entity_extension.h                                     */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "../../openxr_util.h"
+#include "../openxr_extension_wrapper.h"
+#include "core/templates/rid_owner.h"
+#include "core/variant/typed_array.h"
+#include "openxr_spatial_entities.h"
+
+// Spatial entity extension
+class OpenXRSpatialEntityExtension : public OpenXRExtensionWrapper {
+	GDCLASS(OpenXRSpatialEntityExtension, OpenXRExtensionWrapper);
+
+public:
+	enum Capability {
+		CAPABILITY_PLANE_TRACKING = XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT,
+		CAPABILITY_MARKER_TRACKING_QR_CODE = XR_SPATIAL_CAPABILITY_MARKER_TRACKING_QR_CODE_EXT,
+		CAPABILITY_MARKER_TRACKING_MICRO_QR_CODE = XR_SPATIAL_CAPABILITY_MARKER_TRACKING_MICRO_QR_CODE_EXT,
+		CAPABILITY_MARKER_TRACKING_ARUCO_MARKER = XR_SPATIAL_CAPABILITY_MARKER_TRACKING_ARUCO_MARKER_EXT,
+		CAPABILITY_MARKER_TRACKING_APRIL_TAG = XR_SPATIAL_CAPABILITY_MARKER_TRACKING_APRIL_TAG_EXT,
+		CAPABILITY_ANCHOR = XR_SPATIAL_CAPABILITY_ANCHOR_EXT,
+	};
+
+	enum ComponentType {
+		COMPONENT_TYPE_BOUNDED_2D = XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT,
+		COMPONENT_TYPE_BOUNDED_3D = XR_SPATIAL_COMPONENT_TYPE_BOUNDED_3D_EXT,
+		COMPONENT_TYPE_PARENT = XR_SPATIAL_COMPONENT_TYPE_PARENT_EXT,
+		COMPONENT_TYPE_MESH_3D = XR_SPATIAL_COMPONENT_TYPE_MESH_3D_EXT,
+		COMPONENT_TYPE_PLANE_ALIGNMENT = XR_SPATIAL_COMPONENT_TYPE_PLANE_ALIGNMENT_EXT,
+		COMPONENT_TYPE_MESH_2D = XR_SPATIAL_COMPONENT_TYPE_MESH_2D_EXT,
+		COMPONENT_TYPE_POLYGON_2D = XR_SPATIAL_COMPONENT_TYPE_POLYGON_2D_EXT,
+		COMPONENT_TYPE_PLANE_SEMANTIC_LABEL = XR_SPATIAL_COMPONENT_TYPE_PLANE_SEMANTIC_LABEL_EXT,
+		COMPONENT_TYPE_MARKER = XR_SPATIAL_COMPONENT_TYPE_MARKER_EXT,
+		COMPONENT_TYPE_ANCHOR = XR_SPATIAL_COMPONENT_TYPE_ANCHOR_EXT,
+		COMPONENT_TYPE_PERSISTENCE = XR_SPATIAL_COMPONENT_TYPE_PERSISTENCE_EXT,
+	};
+
+	static OpenXRSpatialEntityExtension *get_singleton();
+
+	OpenXRSpatialEntityExtension();
+	virtual ~OpenXRSpatialEntityExtension() override;
+
+	virtual HashMap<String, bool *> get_requested_extensions() override;
+
+	virtual void on_instance_created(const XrInstance p_instance) override;
+	virtual void on_instance_destroyed() override;
+	virtual void on_session_destroyed() override;
+
+	virtual bool on_event_polled(const XrEventDataBuffer &event) override;
+
+	bool get_active() const;
+	bool supports_capability(XrSpatialCapabilityEXT p_capability);
+	bool supports_component_type(XrSpatialCapabilityEXT p_capability, XrSpatialComponentTypeEXT p_component_type);
+
+	// Spatial contexts
+	Ref<OpenXRFutureResult> create_spatial_context(const TypedArray<OpenXRSpatialCapabilityConfigurationBaseHeader> &p_capability_configurations, Ref<OpenXRStructureBase> p_next, const Callable &p_user_callback);
+	bool get_spatial_context_ready(RID p_spatial_context) const;
+	void free_spatial_context(RID p_spatial_context);
+	XrSpatialContextEXT get_spatial_context_handle(RID p_spatial_context) const;
+
+	// Discovery query
+	Ref<OpenXRFutureResult> discover_spatial_entities(RID p_spatial_context, const Vector<XrSpatialComponentTypeEXT> &p_component_types, Ref<OpenXRStructureBase> p_next, const Callable &p_user_callback);
+
+	// Update query
+	RID update_spatial_entities(RID p_spatial_context, const LocalVector<RID> &p_entities, const LocalVector<XrSpatialComponentTypeEXT> &p_component_types, Ref<OpenXRStructureBase> p_next);
+
+	// Snapshot data
+	void free_spatial_snapshot(RID p_spatial_snapshot);
+	XrSpatialSnapshotEXT get_spatial_snapshot_handle(RID p_spatial_snapshot) const;
+	RID get_spatial_snapshot_context(RID p_spatial_snapshot) const;
+
+	bool query_snapshot(RID p_spatial_snapshot, const TypedArray<OpenXRSpatialComponentData> &p_component_data, Ref<OpenXRStructureBase> p_next);
+
+	// Buffers from snapshot
+	String get_string(RID p_spatial_snapshot, XrSpatialBufferIdEXT p_buffer_id) const;
+	PackedByteArray get_uint8_buffer(RID p_spatial_snapshot, XrSpatialBufferIdEXT p_buffer_id) const;
+	Vector<uint16_t> get_uint16_buffer(RID p_spatial_snapshot, XrSpatialBufferIdEXT p_buffer_id) const;
+	Vector<uint32_t> get_uint32_buffer(RID p_spatial_snapshot, XrSpatialBufferIdEXT p_buffer_id) const;
+	PackedFloat32Array get_float_buffer(RID p_spatial_snapshot, XrSpatialBufferIdEXT p_buffer_id) const;
+	PackedVector2Array get_vector2_buffer(RID p_spatial_snapshot, XrSpatialBufferIdEXT p_buffer_id) const;
+	PackedVector3Array get_vector3_buffer(RID p_spatial_snapshot, XrSpatialBufferIdEXT p_buffer_id) const;
+
+	// Entities
+	RID find_spatial_entity(XrSpatialEntityIdEXT p_entity_id) const;
+	RID add_spatial_entity(RID p_spatial_context, XrSpatialEntityIdEXT p_entity_id, XrSpatialEntityEXT p_entity);
+	RID make_spatial_entity(RID p_spatial_context, XrSpatialEntityIdEXT p_entity_id);
+	XrSpatialEntityIdEXT get_spatial_entity_id(RID p_entity) const;
+	RID get_spatial_entity_context(RID p_entity) const;
+	void free_spatial_entity(RID p_entity);
+
+	static String get_spatial_capability_name(XrSpatialCapabilityEXT p_capability);
+	static String get_spatial_component_type_name(XrSpatialComponentTypeEXT p_component_type);
+	static String get_spatial_feature_name(XrSpatialCapabilityFeatureEXT p_feature);
+
+protected:
+	static void _bind_methods();
+
+private:
+	static OpenXRSpatialEntityExtension *singleton;
+
+	bool spatial_entity_ext = false;
+
+	// Capabilities
+	struct SpatialEntityCapabality {
+		Vector<XrSpatialComponentTypeEXT> component_types;
+		Vector<XrSpatialCapabilityFeatureEXT> features;
+	};
+	HashMap<XrSpatialCapabilityEXT, SpatialEntityCapabality> supported_capabilities;
+	int capabilities_load_state = 0; // 0 = no, 1 = yes, 2 = failed
+	bool _load_capabilities();
+
+	bool _supports_capability(Capability p_capability);
+	bool _supports_component_type(Capability p_capability, ComponentType p_component_type);
+
+	// Spatial context
+	struct SpatialContextData {
+		XrSpatialContextEXT spatial_context = XR_NULL_HANDLE;
+	};
+	mutable RID_Owner<SpatialContextData> spatial_context_owner;
+
+	void _on_context_creation_ready(Ref<OpenXRFutureResult> p_future_result, const Callable &p_user_callback);
+	uint64_t _get_spatial_context_handle(RID p_spatial_context) const;
+
+	// Spatial query
+	Ref<OpenXRFutureResult> _discover_spatial_entities(RID p_spatial_context, const PackedInt64Array &p_component_types, Ref<OpenXRStructureBase> p_next, const Callable &p_callback);
+	void _on_discovered_spatial_entities(Ref<OpenXRFutureResult> p_future_result, RID p_discovery_spatial_context, const Callable &p_user_callback);
+
+	// Update query
+	RID _update_spatial_entities(RID p_spatial_context, const TypedArray<RID> &p_entities, const PackedInt64Array &p_component_types, Ref<OpenXRStructureBase> p_next);
+
+	// Snapshot data
+	struct SpatialSnapshotData {
+		RID spatial_context;
+		XrSpatialSnapshotEXT spatial_snapshot = XR_NULL_HANDLE;
+	};
+	mutable RID_Owner<SpatialSnapshotData> spatial_snapshot_owner;
+
+	uint64_t _get_spatial_snapshot_handle(RID p_spatial_snapshot) const;
+
+	// Buffers from snapshot
+	String _get_string(RID p_spatial_snapshot, uint64_t p_buffer_id) const;
+	PackedByteArray _get_uint8_buffer(RID p_spatial_snapshot, uint64_t p_buffer_id) const;
+	PackedInt32Array _get_uint16_buffer(RID p_spatial_snapshot, uint64_t p_buffer_id) const;
+	PackedInt32Array _get_uint32_buffer(RID p_spatial_snapshot, uint64_t p_buffer_id) const;
+	PackedFloat32Array _get_float_buffer(RID p_spatial_snapshot, uint64_t p_buffer_id) const;
+	PackedVector2Array _get_vector2_buffer(RID p_spatial_snapshot, uint64_t p_buffer_id) const;
+	PackedVector3Array _get_vector3_buffer(RID p_spatial_snapshot, uint64_t p_buffer_id) const;
+
+	// Entities
+	struct SpatialEntityData {
+		RID spatial_context;
+		XrSpatialEntityIdEXT entity_id = XR_NULL_ENTITY;
+		XrSpatialEntityEXT entity = XR_NULL_HANDLE;
+	};
+	mutable RID_Owner<SpatialEntityData> spatial_entity_owner;
+
+	RID _find_entity(uint64_t p_entity_id);
+	RID _add_entity(RID p_spatial_context, uint64_t p_entity_id, uint64_t p_entity);
+	RID _make_entity(RID p_spatial_context, uint64_t p_entity_id);
+	uint64_t _get_entity_id(RID p_entity) const;
+
+	// OpenXR API call wrappers
+
+	// Spatial entities
+	EXT_PROTO_XRRESULT_FUNC5(xrEnumerateSpatialCapabilitiesEXT, (XrInstance), instance, (XrSystemId), system_id, (uint32_t), capability_capacity_input, (uint32_t *), capability_count_output, (XrSpatialCapabilityEXT *), capabilities);
+	EXT_PROTO_XRRESULT_FUNC4(xrEnumerateSpatialCapabilityComponentTypesEXT, (XrInstance), instance, (XrSystemId), systemId, (XrSpatialCapabilityEXT), capability, (XrSpatialCapabilityComponentTypesEXT *), capability_components);
+	EXT_PROTO_XRRESULT_FUNC6(xrEnumerateSpatialCapabilityFeaturesEXT, (XrInstance), instance, (XrSystemId), systemId, (XrSpatialCapabilityEXT), capability, (uint32_t), capability_feature_capacity_input, (uint32_t *), capability_feature_count_output, (XrSpatialCapabilityFeatureEXT *), capability_features);
+	EXT_PROTO_XRRESULT_FUNC3(xrCreateSpatialContextAsyncEXT, (XrSession), session, (const XrSpatialContextCreateInfoEXT *), create_info, (XrFutureEXT *), future);
+	EXT_PROTO_XRRESULT_FUNC3(xrCreateSpatialContextCompleteEXT, (XrSession), session, (XrFutureEXT), future, (XrCreateSpatialContextCompletionEXT *), completion);
+	EXT_PROTO_XRRESULT_FUNC1(xrDestroySpatialContextEXT, (XrSpatialContextEXT), spatial_context);
+	EXT_PROTO_XRRESULT_FUNC3(xrCreateSpatialDiscoverySnapshotAsyncEXT, (XrSpatialContextEXT), spatial_context, (const XrSpatialDiscoverySnapshotCreateInfoEXT *), create_info, (XrFutureEXT *), future);
+	EXT_PROTO_XRRESULT_FUNC3(xrCreateSpatialDiscoverySnapshotCompleteEXT, (XrSpatialContextEXT), spatial_context, (const XrCreateSpatialDiscoverySnapshotCompletionInfoEXT *), create_snapshot_completion_info, (XrCreateSpatialDiscoverySnapshotCompletionEXT *), completion);
+	EXT_PROTO_XRRESULT_FUNC3(xrQuerySpatialComponentDataEXT, (XrSpatialSnapshotEXT), snapshot, (const XrSpatialComponentDataQueryConditionEXT *), query_condition, (XrSpatialComponentDataQueryResultEXT *), query_result);
+	EXT_PROTO_XRRESULT_FUNC1(xrDestroySpatialSnapshotEXT, (XrSpatialSnapshotEXT), snapshot);
+	EXT_PROTO_XRRESULT_FUNC3(xrCreateSpatialEntityFromIdEXT, (XrSpatialContextEXT), spatial_context, (const XrSpatialEntityFromIdCreateInfoEXT *), create_info, (XrSpatialEntityEXT *), spatial_entity);
+	EXT_PROTO_XRRESULT_FUNC1(xrDestroySpatialEntityEXT, (XrSpatialEntityEXT), spatial_entity);
+	EXT_PROTO_XRRESULT_FUNC3(xrCreateSpatialUpdateSnapshotEXT, (XrSpatialContextEXT), spatial_context, (const XrSpatialUpdateSnapshotCreateInfoEXT *), createInfo, (XrSpatialSnapshotEXT *), snapshot);
+	EXT_PROTO_XRRESULT_FUNC5(xrGetSpatialBufferStringEXT, (XrSpatialSnapshotEXT), snapshot, (const XrSpatialBufferGetInfoEXT *), info, (uint32_t), buffer_capacity_input, (uint32_t *), buffer_count_output, (char *), buffer);
+	EXT_PROTO_XRRESULT_FUNC5(xrGetSpatialBufferUint8EXT, (XrSpatialSnapshotEXT), snapshot, (const XrSpatialBufferGetInfoEXT *), info, (uint32_t), buffer_capacity_input, (uint32_t *), buffer_count_output, (uint8_t *), buffer);
+	EXT_PROTO_XRRESULT_FUNC5(xrGetSpatialBufferUint16EXT, (XrSpatialSnapshotEXT), snapshot, (const XrSpatialBufferGetInfoEXT *), info, (uint32_t), buffer_capacity_input, (uint32_t *), buffer_count_output, (uint16_t *), buffer);
+	EXT_PROTO_XRRESULT_FUNC5(xrGetSpatialBufferUint32EXT, (XrSpatialSnapshotEXT), snapshot, (const XrSpatialBufferGetInfoEXT *), info, (uint32_t), buffer_capacity_input, (uint32_t *), buffer_count_output, (uint32_t *), buffer);
+	EXT_PROTO_XRRESULT_FUNC5(xrGetSpatialBufferFloatEXT, (XrSpatialSnapshotEXT), snapshot, (const XrSpatialBufferGetInfoEXT *), info, (uint32_t), buffer_capacity_input, (uint32_t *), buffer_count_output, (float *), buffer);
+	EXT_PROTO_XRRESULT_FUNC5(xrGetSpatialBufferVector2fEXT, (XrSpatialSnapshotEXT), snapshot, (const XrSpatialBufferGetInfoEXT *), info, (uint32_t), buffer_capacity_input, (uint32_t *), buffer_count_output, (XrVector2f *), buffer);
+	EXT_PROTO_XRRESULT_FUNC5(xrGetSpatialBufferVector3fEXT, (XrSpatialSnapshotEXT), snapshot, (const XrSpatialBufferGetInfoEXT *), info, (uint32_t), buffer_capacity_input, (uint32_t *), buffer_count_output, (XrVector3f *), buffer);
+};
+
+VARIANT_ENUM_CAST(OpenXRSpatialEntityExtension::Capability);
+VARIANT_ENUM_CAST(OpenXRSpatialEntityExtension::ComponentType);

+ 788 - 0
modules/openxr/extensions/spatial_entities/openxr_spatial_marker_tracking.cpp

@@ -0,0 +1,788 @@
+/**************************************************************************/
+/*  openxr_spatial_marker_tracking.cpp                                    */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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 "openxr_spatial_marker_tracking.h"
+
+#include "../../openxr_api.h"
+#include "core/config/project_settings.h"
+#include "openxr_spatial_entity_extension.h"
+#include "servers/xr_server.h"
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialCapabilityConfigurationQrCode
+
+void OpenXRSpatialCapabilityConfigurationQrCode::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_enabled_components"), &OpenXRSpatialCapabilityConfigurationQrCode::_get_enabled_components);
+}
+
+bool OpenXRSpatialCapabilityConfigurationQrCode::has_valid_configuration() const {
+	OpenXRSpatialMarkerTrackingCapability *capability = OpenXRSpatialMarkerTrackingCapability::get_singleton();
+	ERR_FAIL_NULL_V(capability, false);
+
+	return capability->is_qrcode_supported();
+}
+
+XrSpatialCapabilityConfigurationBaseHeaderEXT *OpenXRSpatialCapabilityConfigurationQrCode::get_configuration() {
+	OpenXRSpatialMarkerTrackingCapability *capability = OpenXRSpatialMarkerTrackingCapability::get_singleton();
+	ERR_FAIL_NULL_V(capability, nullptr);
+
+	if (capability->is_qrcode_supported()) {
+		OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+		ERR_FAIL_NULL_V(se_extension, nullptr);
+
+		// Guaranteed components:
+		enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_MARKER_EXT);
+		enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT);
+
+		// Set up our enabled components.
+		marker_config.enabledComponentCount = enabled_components.size();
+		marker_config.enabledComponents = enabled_components.ptr();
+
+		// and return this.
+		return (XrSpatialCapabilityConfigurationBaseHeaderEXT *)&marker_config;
+	}
+
+	return nullptr;
+}
+
+PackedInt64Array OpenXRSpatialCapabilityConfigurationQrCode::_get_enabled_components() const {
+	PackedInt64Array components;
+
+	for (const XrSpatialComponentTypeEXT &component_type : enabled_components) {
+		components.push_back((int64_t)component_type);
+	}
+
+	return components;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialCapabilityConfigurationMicroQrCode
+
+void OpenXRSpatialCapabilityConfigurationMicroQrCode::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_enabled_components"), &OpenXRSpatialCapabilityConfigurationMicroQrCode::_get_enabled_components);
+}
+
+bool OpenXRSpatialCapabilityConfigurationMicroQrCode::has_valid_configuration() const {
+	OpenXRSpatialMarkerTrackingCapability *capability = OpenXRSpatialMarkerTrackingCapability::get_singleton();
+	ERR_FAIL_NULL_V(capability, false);
+
+	return capability->is_micro_qrcode_supported();
+}
+
+XrSpatialCapabilityConfigurationBaseHeaderEXT *OpenXRSpatialCapabilityConfigurationMicroQrCode::get_configuration() {
+	OpenXRSpatialMarkerTrackingCapability *capability = OpenXRSpatialMarkerTrackingCapability::get_singleton();
+	ERR_FAIL_NULL_V(capability, nullptr);
+
+	if (capability->is_micro_qrcode_supported()) {
+		OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+		ERR_FAIL_NULL_V(se_extension, nullptr);
+
+		// Guaranteed components:
+		enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_MARKER_EXT);
+		enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT);
+
+		// Set up our enabled components.
+		marker_config.enabledComponentCount = enabled_components.size();
+		marker_config.enabledComponents = enabled_components.ptr();
+
+		// and return this.
+		return (XrSpatialCapabilityConfigurationBaseHeaderEXT *)&marker_config;
+	}
+
+	return nullptr;
+}
+
+PackedInt64Array OpenXRSpatialCapabilityConfigurationMicroQrCode::_get_enabled_components() const {
+	PackedInt64Array components;
+
+	for (const XrSpatialComponentTypeEXT &component_type : enabled_components) {
+		components.push_back((int64_t)component_type);
+	}
+
+	return components;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialCapabilityConfigurationAruco
+
+void OpenXRSpatialCapabilityConfigurationAruco::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_enabled_components"), &OpenXRSpatialCapabilityConfigurationAruco::_get_enabled_components);
+
+	ClassDB::bind_method(D_METHOD("set_aruco_dict", "aruco_dict"), &OpenXRSpatialCapabilityConfigurationAruco::_set_aruco_dict);
+	ClassDB::bind_method(D_METHOD("get_aruco_dict"), &OpenXRSpatialCapabilityConfigurationAruco::_get_aruco_dict);
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "aruco_dict"), "set_aruco_dict", "get_aruco_dict");
+
+	BIND_ENUM_CONSTANT(ARUCO_DICT_4X4_50);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_4X4_100);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_4X4_250);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_4X4_1000);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_5X5_50);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_5X5_100);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_5X5_250);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_5X5_1000);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_6X6_50);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_6X6_100);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_6X6_250);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_6X6_1000);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_7X7_50);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_7X7_100);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_7X7_250);
+	BIND_ENUM_CONSTANT(ARUCO_DICT_7X7_1000);
+}
+
+bool OpenXRSpatialCapabilityConfigurationAruco::has_valid_configuration() const {
+	OpenXRSpatialMarkerTrackingCapability *capability = OpenXRSpatialMarkerTrackingCapability::get_singleton();
+	ERR_FAIL_NULL_V(capability, false);
+
+	return capability->is_aruco_supported();
+}
+
+XrSpatialCapabilityConfigurationBaseHeaderEXT *OpenXRSpatialCapabilityConfigurationAruco::get_configuration() {
+	OpenXRSpatialMarkerTrackingCapability *capability = OpenXRSpatialMarkerTrackingCapability::get_singleton();
+	ERR_FAIL_NULL_V(capability, nullptr);
+
+	if (capability->is_aruco_supported()) {
+		OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+		ERR_FAIL_NULL_V(se_extension, nullptr);
+
+		// Guaranteed components:
+		enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_MARKER_EXT);
+		enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT);
+
+		// Set up our enabled components.
+		marker_config.enabledComponentCount = enabled_components.size();
+		marker_config.enabledComponents = enabled_components.ptr();
+
+		// and return this.
+		return (XrSpatialCapabilityConfigurationBaseHeaderEXT *)&marker_config;
+	}
+
+	return nullptr;
+}
+
+void OpenXRSpatialCapabilityConfigurationAruco::set_aruco_dict(XrSpatialMarkerArucoDictEXT p_dict) {
+	marker_config.arUcoDict = p_dict;
+}
+
+void OpenXRSpatialCapabilityConfigurationAruco::_set_aruco_dict(ArucoDict p_dict) {
+	set_aruco_dict((XrSpatialMarkerArucoDictEXT)p_dict);
+}
+
+XrSpatialMarkerArucoDictEXT OpenXRSpatialCapabilityConfigurationAruco::get_aruco_dict() const {
+	return marker_config.arUcoDict;
+}
+
+OpenXRSpatialCapabilityConfigurationAruco::ArucoDict OpenXRSpatialCapabilityConfigurationAruco::_get_aruco_dict() const {
+	return (ArucoDict)get_aruco_dict();
+}
+
+PackedInt64Array OpenXRSpatialCapabilityConfigurationAruco::_get_enabled_components() const {
+	PackedInt64Array components;
+
+	for (const XrSpatialComponentTypeEXT &component_type : enabled_components) {
+		components.push_back((int64_t)component_type);
+	}
+
+	return components;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialCapabilityConfigurationAprilTag
+
+void OpenXRSpatialCapabilityConfigurationAprilTag::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_enabled_components"), &OpenXRSpatialCapabilityConfigurationAprilTag::_get_enabled_components);
+
+	ClassDB::bind_method(D_METHOD("set_april_dict", "april_dict"), &OpenXRSpatialCapabilityConfigurationAprilTag::_set_april_dict);
+	ClassDB::bind_method(D_METHOD("get_april_dict"), &OpenXRSpatialCapabilityConfigurationAprilTag::_get_april_dict);
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "april_dict"), "set_april_dict", "get_april_dict");
+
+	BIND_ENUM_CONSTANT(APRIL_TAG_DICT_16H5);
+	BIND_ENUM_CONSTANT(APRIL_TAG_DICT_25H9);
+	BIND_ENUM_CONSTANT(APRIL_TAG_DICT_36H10);
+	BIND_ENUM_CONSTANT(APRIL_TAG_DICT_36H11);
+}
+
+bool OpenXRSpatialCapabilityConfigurationAprilTag::has_valid_configuration() const {
+	OpenXRSpatialMarkerTrackingCapability *capability = OpenXRSpatialMarkerTrackingCapability::get_singleton();
+	ERR_FAIL_NULL_V(capability, false);
+
+	return capability->is_april_tag_supported();
+}
+
+XrSpatialCapabilityConfigurationBaseHeaderEXT *OpenXRSpatialCapabilityConfigurationAprilTag::get_configuration() {
+	OpenXRSpatialMarkerTrackingCapability *capability = OpenXRSpatialMarkerTrackingCapability::get_singleton();
+	ERR_FAIL_NULL_V(capability, nullptr);
+
+	if (capability->is_april_tag_supported()) {
+		OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+		ERR_FAIL_NULL_V(se_extension, nullptr);
+
+		// Guaranteed components:
+		enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_MARKER_EXT);
+		enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT);
+
+		// Set up our enabled components.
+		marker_config.enabledComponentCount = enabled_components.size();
+		marker_config.enabledComponents = enabled_components.ptr();
+
+		// and return this.
+		return (XrSpatialCapabilityConfigurationBaseHeaderEXT *)&marker_config;
+	}
+
+	return nullptr;
+}
+
+void OpenXRSpatialCapabilityConfigurationAprilTag::set_april_dict(XrSpatialMarkerAprilTagDictEXT p_dict) {
+	marker_config.aprilDict = p_dict;
+}
+
+void OpenXRSpatialCapabilityConfigurationAprilTag::_set_april_dict(AprilTagDict p_dict) {
+	set_april_dict((XrSpatialMarkerAprilTagDictEXT)p_dict);
+}
+
+XrSpatialMarkerAprilTagDictEXT OpenXRSpatialCapabilityConfigurationAprilTag::get_april_dict() const {
+	return marker_config.aprilDict;
+}
+
+OpenXRSpatialCapabilityConfigurationAprilTag::AprilTagDict OpenXRSpatialCapabilityConfigurationAprilTag::_get_april_dict() const {
+	return (AprilTagDict)get_april_dict();
+}
+
+PackedInt64Array OpenXRSpatialCapabilityConfigurationAprilTag::_get_enabled_components() const {
+	PackedInt64Array components;
+
+	for (const XrSpatialComponentTypeEXT &component_type : enabled_components) {
+		components.push_back((int64_t)component_type);
+	}
+
+	return components;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialComponentMarkerList
+
+void OpenXRSpatialComponentMarkerList::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_marker_type", "index"), &OpenXRSpatialComponentMarkerList::get_marker_type);
+	ClassDB::bind_method(D_METHOD("get_marker_id", "index"), &OpenXRSpatialComponentMarkerList::get_marker_id);
+	ClassDB::bind_method(D_METHOD("get_marker_data", "snapshot", "index"), &OpenXRSpatialComponentMarkerList::get_marker_data);
+
+	BIND_ENUM_CONSTANT(MARKER_TYPE_UNKNOWN);
+	BIND_ENUM_CONSTANT(MARKER_TYPE_QRCODE);
+	BIND_ENUM_CONSTANT(MARKER_TYPE_MICRO_QRCODE);
+	BIND_ENUM_CONSTANT(MARKER_TYPE_ARUCO);
+	BIND_ENUM_CONSTANT(MARKER_TYPE_APRIL_TAG);
+	BIND_ENUM_CONSTANT(MARKER_TYPE_MAX);
+}
+
+void OpenXRSpatialComponentMarkerList::set_capacity(uint32_t p_capacity) {
+	marker_data.resize(p_capacity);
+
+	marker_list.markerCount = uint32_t(marker_data.size());
+	marker_list.markers = marker_data.ptrw();
+}
+
+XrSpatialComponentTypeEXT OpenXRSpatialComponentMarkerList::get_component_type() const {
+	return XR_SPATIAL_COMPONENT_TYPE_MARKER_EXT;
+}
+
+void *OpenXRSpatialComponentMarkerList::get_structure_data(void *p_next) {
+	marker_list.next = p_next;
+	return &marker_list;
+}
+
+OpenXRSpatialComponentMarkerList::MarkerType OpenXRSpatialComponentMarkerList::get_marker_type(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, marker_data.size(), MARKER_TYPE_UNKNOWN);
+
+	// We can't simply cast these.
+	// This may give us problems in the future if we get new types through vendor extensions.
+	switch (marker_data[p_index].capability) {
+		case XR_SPATIAL_CAPABILITY_MARKER_TRACKING_QR_CODE_EXT: {
+			return MARKER_TYPE_QRCODE;
+		} break;
+		case XR_SPATIAL_CAPABILITY_MARKER_TRACKING_MICRO_QR_CODE_EXT: {
+			return MARKER_TYPE_MICRO_QRCODE;
+		} break;
+		case XR_SPATIAL_CAPABILITY_MARKER_TRACKING_ARUCO_MARKER_EXT: {
+			return MARKER_TYPE_ARUCO;
+		} break;
+		case XR_SPATIAL_CAPABILITY_MARKER_TRACKING_APRIL_TAG_EXT: {
+			return MARKER_TYPE_APRIL_TAG;
+		} break;
+		default: {
+			return MARKER_TYPE_UNKNOWN;
+		} break;
+	}
+}
+
+uint32_t OpenXRSpatialComponentMarkerList::get_marker_id(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, marker_data.size(), 0);
+
+	return marker_data[p_index].markerId;
+}
+
+Variant OpenXRSpatialComponentMarkerList::get_marker_data(RID p_snapshot, int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, marker_data.size(), Variant());
+
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, Variant());
+
+	const XrSpatialBufferEXT &data = marker_data[p_index].data;
+	switch (data.bufferType) {
+		case XR_SPATIAL_BUFFER_TYPE_STRING_EXT: {
+			return se_extension->get_string(p_snapshot, data.bufferId);
+		} break;
+		case XR_SPATIAL_BUFFER_TYPE_UINT8_EXT: {
+			return se_extension->get_uint8_buffer(p_snapshot, data.bufferId);
+		} break;
+		default: {
+			return Variant();
+		} break;
+	}
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRMarkerTracker
+
+void OpenXRMarkerTracker::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("set_bounds_size", "bounds_size"), &OpenXRMarkerTracker::set_bounds_size);
+	ClassDB::bind_method(D_METHOD("get_bounds_size"), &OpenXRMarkerTracker::get_bounds_size);
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "bounds_size"), "set_bounds_size", "get_bounds_size");
+
+	ClassDB::bind_method(D_METHOD("set_marker_type", "marker_type"), &OpenXRMarkerTracker::set_marker_type);
+	ClassDB::bind_method(D_METHOD("get_marker_type"), &OpenXRMarkerTracker::get_marker_type);
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "marker_type"), "set_marker_type", "get_marker_type");
+
+	ClassDB::bind_method(D_METHOD("set_marker_id", "marker_id"), &OpenXRMarkerTracker::set_marker_id);
+	ClassDB::bind_method(D_METHOD("get_marker_id"), &OpenXRMarkerTracker::get_marker_id);
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "marker_id"), "set_marker_id", "get_marker_id");
+
+	// As the type of marker data can vary, we can't make this a property.
+	ClassDB::bind_method(D_METHOD("set_marker_data", "marker_data"), &OpenXRMarkerTracker::set_marker_data);
+	ClassDB::bind_method(D_METHOD("get_marker_data"), &OpenXRMarkerTracker::get_marker_data);
+}
+
+void OpenXRMarkerTracker::set_bounds_size(const Vector2 &p_bounds_size) {
+	bounds_size = p_bounds_size;
+}
+
+Vector2 OpenXRMarkerTracker::get_bounds_size() const {
+	return bounds_size;
+}
+
+void OpenXRMarkerTracker::set_marker_type(OpenXRSpatialComponentMarkerList::MarkerType p_marker_type) {
+	marker_type = p_marker_type;
+}
+
+OpenXRSpatialComponentMarkerList::MarkerType OpenXRMarkerTracker::get_marker_type() const {
+	return marker_type;
+}
+
+void OpenXRMarkerTracker::set_marker_id(uint32_t p_id) {
+	marker_id = p_id;
+}
+
+uint32_t OpenXRMarkerTracker::get_marker_id() const {
+	return marker_id;
+}
+
+void OpenXRMarkerTracker::set_marker_data(const Variant &p_data) {
+	marker_data = p_data;
+}
+
+Variant OpenXRMarkerTracker::get_marker_data() const {
+	return marker_data;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialMarkerTrackingCapability
+
+OpenXRSpatialMarkerTrackingCapability *OpenXRSpatialMarkerTrackingCapability::singleton = nullptr;
+
+OpenXRSpatialMarkerTrackingCapability *OpenXRSpatialMarkerTrackingCapability::get_singleton() {
+	return singleton;
+}
+
+OpenXRSpatialMarkerTrackingCapability::OpenXRSpatialMarkerTrackingCapability() {
+	singleton = this;
+}
+
+OpenXRSpatialMarkerTrackingCapability::~OpenXRSpatialMarkerTrackingCapability() {
+	singleton = nullptr;
+}
+
+void OpenXRSpatialMarkerTrackingCapability::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("is_qrcode_supported"), &OpenXRSpatialMarkerTrackingCapability::is_qrcode_supported);
+	ClassDB::bind_method(D_METHOD("is_micro_qrcode_supported"), &OpenXRSpatialMarkerTrackingCapability::is_micro_qrcode_supported);
+	ClassDB::bind_method(D_METHOD("is_aruco_supported"), &OpenXRSpatialMarkerTrackingCapability::is_aruco_supported);
+	ClassDB::bind_method(D_METHOD("is_april_tag_supported"), &OpenXRSpatialMarkerTrackingCapability::is_april_tag_supported);
+}
+
+HashMap<String, bool *> OpenXRSpatialMarkerTrackingCapability::get_requested_extensions() {
+	HashMap<String, bool *> request_extensions;
+
+	if (GLOBAL_GET_CACHED(bool, "xr/openxr/extensions/spatial_entity/enabled") && GLOBAL_GET_CACHED(bool, "xr/openxr/extensions/spatial_entity/enable_marker_tracking")) {
+		request_extensions[XR_EXT_SPATIAL_MARKER_TRACKING_EXTENSION_NAME] = &spatial_marker_tracking_ext;
+	}
+
+	return request_extensions;
+}
+
+void OpenXRSpatialMarkerTrackingCapability::on_session_created(const XrSession p_session) {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL(se_extension);
+
+	if (!spatial_marker_tracking_ext) {
+		return;
+	}
+
+	se_extension->connect(SNAME("spatial_discovery_recommended"), callable_mp(this, &OpenXRSpatialMarkerTrackingCapability::_on_spatial_discovery_recommended));
+
+	if (GLOBAL_GET_CACHED(bool, "xr/openxr/extensions/spatial_entity/enable_builtin_marker_tracking")) {
+		// Start by creating our spatial context
+		_create_spatial_context();
+	}
+}
+
+void OpenXRSpatialMarkerTrackingCapability::on_session_destroyed() {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL(se_extension);
+	XRServer *xr_server = XRServer::get_singleton();
+	ERR_FAIL_NULL(xr_server);
+
+	// Free and unregister our anchors
+	for (const KeyValue<XrSpatialEntityIdEXT, Ref<OpenXRMarkerTracker>> &marker_tracker : marker_trackers) {
+		xr_server->remove_tracker(marker_tracker.value);
+	}
+	marker_trackers.clear();
+
+	// Free our spatial context
+	if (spatial_context.is_valid()) {
+		se_extension->free_spatial_context(spatial_context);
+		spatial_context = RID();
+	}
+
+	se_extension->disconnect(SNAME("spatial_discovery_recommended"), callable_mp(this, &OpenXRSpatialMarkerTrackingCapability::_on_spatial_discovery_recommended));
+}
+
+void OpenXRSpatialMarkerTrackingCapability::on_process() {
+	if (!spatial_context.is_valid()) {
+		return;
+	}
+
+	// Protection against marker discovery happening too often.
+	if (discovery_cooldown > 0) {
+		discovery_cooldown--;
+	}
+
+	// Check if we need to start our discovery.
+	if (need_discovery && discovery_cooldown == 0 && !discovery_query_result.is_valid()) {
+		need_discovery = false;
+		discovery_cooldown = 60; // Set our cooldown to 60 frames, it doesn't need to be an exact science.
+
+		_start_entity_discovery();
+	}
+
+	// If we have markers, we do an update query to check for changed positions.
+	if (!marker_trackers.is_empty()) {
+		OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+		ERR_FAIL_NULL(se_extension);
+
+		// We want updates for all anchors
+		thread_local LocalVector<RID> entities;
+		entities.resize(marker_trackers.size());
+		RID *entity = entities.ptr();
+		for (const KeyValue<XrSpatialEntityIdEXT, Ref<OpenXRMarkerTracker>> &e : marker_trackers) {
+			*entity = e.value->get_entity();
+			entity++;
+		}
+
+		// We just want our anchor component
+		thread_local LocalVector<XrSpatialComponentTypeEXT> component_types;
+		component_types.push_back(XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT);
+
+		// And we get our update snapshot, this is NOT async!
+		RID snapshot = se_extension->update_spatial_entities(spatial_context, entities, component_types, nullptr);
+		if (snapshot.is_valid()) {
+			_process_snapshot(snapshot, false);
+		}
+	}
+}
+
+bool OpenXRSpatialMarkerTrackingCapability::is_qrcode_supported() {
+	if (!spatial_marker_tracking_ext) {
+		return false;
+	}
+
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, false);
+
+	return se_extension->supports_capability(XR_SPATIAL_CAPABILITY_MARKER_TRACKING_QR_CODE_EXT);
+}
+
+bool OpenXRSpatialMarkerTrackingCapability::is_micro_qrcode_supported() {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, false);
+
+	return se_extension->supports_capability(XR_SPATIAL_CAPABILITY_MARKER_TRACKING_MICRO_QR_CODE_EXT);
+}
+
+bool OpenXRSpatialMarkerTrackingCapability::is_aruco_supported() {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, false);
+
+	return se_extension->supports_capability(XR_SPATIAL_CAPABILITY_MARKER_TRACKING_ARUCO_MARKER_EXT);
+}
+
+bool OpenXRSpatialMarkerTrackingCapability::is_april_tag_supported() {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, false);
+
+	return se_extension->supports_capability(XR_SPATIAL_CAPABILITY_MARKER_TRACKING_APRIL_TAG_EXT);
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Discovery logic
+
+Ref<OpenXRFutureResult> OpenXRSpatialMarkerTrackingCapability::_create_spatial_context() {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, nullptr);
+
+	TypedArray<OpenXRSpatialCapabilityConfigurationBaseHeader> capability_configurations;
+
+	// Create our configuration objects.
+	// For now we enable all supported markers, will need to give some more user control over this.
+	if (is_qrcode_supported()) {
+		qrcode_configuration.instantiate();
+		capability_configurations.push_back(qrcode_configuration);
+	}
+
+	if (is_micro_qrcode_supported()) {
+		micro_qrcode_configuration.instantiate();
+		capability_configurations.push_back(micro_qrcode_configuration);
+	}
+
+	if (is_aruco_supported()) {
+		aruco_configuration.instantiate();
+
+		int aruco_dict = GLOBAL_GET_CACHED(int, "xr/openxr/extensions/spatial_entity/aruco_dict");
+		aruco_configuration->set_aruco_dict((XrSpatialMarkerArucoDictEXT)(XR_SPATIAL_MARKER_ARUCO_DICT_4X4_50_EXT + aruco_dict));
+		capability_configurations.push_back(aruco_configuration);
+	}
+
+	if (is_april_tag_supported()) {
+		april_tag_configuration.instantiate();
+
+		int april_tag_dict = GLOBAL_GET_CACHED(int, "xr/openxr/extensions/spatial_entity/april_tag_dict");
+		april_tag_configuration->set_april_dict((XrSpatialMarkerAprilTagDictEXT)(XR_SPATIAL_MARKER_APRIL_TAG_DICT_16H5_EXT + april_tag_dict));
+		capability_configurations.push_back(april_tag_configuration);
+	}
+
+	if (capability_configurations.is_empty()) {
+		print_verbose("OpenXR: There are no supported marker types. Marker tracking is not enabled.");
+		return nullptr;
+	}
+
+	return se_extension->create_spatial_context(capability_configurations, nullptr, callable_mp(this, &OpenXRSpatialMarkerTrackingCapability::_on_spatial_context_created));
+}
+
+void OpenXRSpatialMarkerTrackingCapability::_on_spatial_context_created(RID p_spatial_context) {
+	spatial_context = p_spatial_context;
+	need_discovery = true;
+}
+
+void OpenXRSpatialMarkerTrackingCapability::_on_spatial_discovery_recommended(RID p_spatial_context) {
+	if (p_spatial_context == spatial_context) {
+		// Trigger new discovery.
+		need_discovery = true;
+	}
+}
+
+Ref<OpenXRFutureResult> OpenXRSpatialMarkerTrackingCapability::_start_entity_discovery() {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, nullptr);
+
+	// Already running or ran discovery, cancel/clean up.
+	if (discovery_query_result.is_valid()) {
+		discovery_query_result->cancel_future();
+		discovery_query_result.unref();
+	}
+
+	// We want both our anchor and persistence component.
+	Vector<XrSpatialComponentTypeEXT> component_types;
+	component_types.push_back(XR_SPATIAL_COMPONENT_TYPE_MARKER_EXT);
+	component_types.push_back(XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT);
+
+	// Start our new snapshot.
+	discovery_query_result = se_extension->discover_spatial_entities(spatial_context, component_types, nullptr, callable_mp(this, &OpenXRSpatialMarkerTrackingCapability::_process_snapshot).bind(true));
+
+	return discovery_query_result;
+}
+
+void OpenXRSpatialMarkerTrackingCapability::_process_snapshot(RID p_snapshot, bool p_is_discovery) {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL(se_extension);
+	XRServer *xr_server = XRServer::get_singleton();
+	ERR_FAIL_NULL(xr_server);
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+
+	// Make a copy of the markers we have right now, so we know which ones to clean up.
+	LocalVector<XrSpatialEntityIdEXT> current_markers;
+	if (p_is_discovery) {
+		current_markers.resize(marker_trackers.size());
+		int m = 0;
+		for (const KeyValue<XrSpatialEntityIdEXT, Ref<OpenXRMarkerTracker>> &marker : marker_trackers) {
+			current_markers[m++] = marker.key;
+		}
+	}
+
+	// Build our component data.
+	TypedArray<OpenXRSpatialComponentData> component_data;
+
+	// We always need a query result data object.
+	Ref<OpenXRSpatialQueryResultData> query_result_data;
+	query_result_data.instantiate();
+	component_data.push_back(query_result_data);
+
+	// Add bounded2D.
+	Ref<OpenXRSpatialComponentBounded2DList> bounded2d_list;
+	bounded2d_list.instantiate();
+	component_data.push_back(bounded2d_list);
+
+	// Marker data list.
+	Ref<OpenXRSpatialComponentMarkerList> marker_list;
+	if (p_is_discovery) {
+		marker_list.instantiate();
+		component_data.push_back(marker_list);
+	}
+
+	if (se_extension->query_snapshot(p_snapshot, component_data, nullptr)) {
+		// Now loop through our data and update our markers.
+		int64_t size = query_result_data->get_capacity();
+
+		for (int64_t i = 0; i < size; i++) {
+			XrSpatialEntityIdEXT entity_id = query_result_data->get_entity_id(i);
+			XrSpatialEntityTrackingStateEXT entity_state = query_result_data->get_entity_state(i);
+
+			// Erase it from our current markers (if we have it, else this is ignored).
+			current_markers.erase(entity_id);
+
+			if (entity_state == XR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED_EXT) {
+				// We should only get this status on update queries.
+				// We'll remove the marker.
+				if (marker_trackers.has(entity_id)) {
+					Ref<OpenXRMarkerTracker> marker_tracker = marker_trackers[entity_id];
+
+					marker_tracker->invalidate_pose(SNAME("default"));
+					marker_tracker->set_spatial_tracking_state(XR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED_EXT);
+
+					// Remove it from our XRServer.
+					xr_server->remove_tracker(marker_tracker);
+
+					// Remove it from our trackers.
+					marker_trackers.erase(entity_id);
+				}
+			} else {
+				// Process our entity.
+				bool add_to_xr_server = false;
+				Ref<OpenXRMarkerTracker> marker_tracker;
+
+				if (marker_trackers.has(entity_id)) {
+					// We know about this one already.
+					marker_tracker = marker_trackers[entity_id];
+				} else {
+					// Create a new anchor.
+					marker_tracker.instantiate();
+					marker_tracker->set_entity(se_extension->make_spatial_entity(se_extension->get_spatial_snapshot_context(p_snapshot), entity_id));
+					marker_trackers[entity_id] = marker_tracker;
+
+					add_to_xr_server = true;
+				}
+
+				// Handle component data.
+				if (entity_state == XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT) {
+					marker_tracker->invalidate_pose(SNAME("default"));
+					marker_tracker->set_spatial_tracking_state(XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT);
+
+					// No further component data will be valid in this state, we need to ignore it!
+				} else if (entity_state == XR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING_EXT) {
+					Transform3D transform = bounded2d_list->get_center_pose(i);
+					marker_tracker->set_pose(SNAME("default"), transform, Vector3(), Vector3());
+					marker_tracker->set_spatial_tracking_state(XR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING_EXT);
+
+					// Process our component data.
+
+					// Set bounds size.
+					marker_tracker->set_bounds_size(bounded2d_list->get_size(i));
+
+					// Set marker data.
+					if (p_is_discovery) {
+						marker_tracker->set_marker_type(marker_list->get_marker_type(i));
+						marker_tracker->set_marker_id(marker_list->get_marker_id(i));
+						marker_tracker->set_marker_data(marker_list->get_marker_data(p_snapshot, i));
+					}
+				}
+
+				if (add_to_xr_server) {
+					// Register with XR server.
+					xr_server->add_tracker(marker_tracker);
+				}
+			}
+		}
+
+		if (p_is_discovery) {
+			// Remove any markers that are no longer there...
+			for (const XrSpatialEntityIdEXT &entity_id : current_markers) {
+				if (marker_trackers.has(entity_id)) {
+					Ref<OpenXRMarkerTracker> marker_tracker = marker_trackers[entity_id];
+
+					// Just in case there are still references out there to this marker,
+					// reset some stuff.
+					marker_tracker->invalidate_pose(SNAME("default"));
+					marker_tracker->set_spatial_tracking_state(XR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED_EXT);
+
+					// Remove it from our XRServer.
+					xr_server->remove_tracker(marker_tracker);
+
+					// Remove it from our trackers.
+					marker_trackers.erase(entity_id);
+				}
+			}
+		}
+	}
+
+	// Now that we're done, clean up our snapshot!
+	se_extension->free_spatial_snapshot(p_snapshot);
+
+	// And if this was our discovery snapshot, let's reset it.
+	if (p_is_discovery && discovery_query_result.is_valid() && discovery_query_result->get_result_value() == p_snapshot) {
+		discovery_query_result.unref();
+	}
+}

+ 268 - 0
modules/openxr/extensions/spatial_entities/openxr_spatial_marker_tracking.h

@@ -0,0 +1,268 @@
+/**************************************************************************/
+/*  openxr_spatial_marker_tracking.h                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "openxr_spatial_entities.h"
+
+// QrCode marker tracking capability configuration
+class OpenXRSpatialCapabilityConfigurationQrCode : public OpenXRSpatialCapabilityConfigurationBaseHeader {
+	GDCLASS(OpenXRSpatialCapabilityConfigurationQrCode, OpenXRSpatialCapabilityConfigurationBaseHeader);
+
+public:
+	virtual bool has_valid_configuration() const override;
+	virtual XrSpatialCapabilityConfigurationBaseHeaderEXT *get_configuration() override;
+
+	Vector<XrSpatialComponentTypeEXT> get_enabled_components() const { return enabled_components; }
+
+protected:
+	static void _bind_methods();
+
+private:
+	Vector<XrSpatialComponentTypeEXT> enabled_components;
+	XrSpatialCapabilityConfigurationQrCodeEXT marker_config = { XR_TYPE_SPATIAL_CAPABILITY_CONFIGURATION_QR_CODE_EXT, nullptr, XR_SPATIAL_CAPABILITY_MARKER_TRACKING_QR_CODE_EXT, 0, nullptr };
+
+	PackedInt64Array _get_enabled_components() const;
+};
+
+// Micro QrCode marker tracking capability configuration
+class OpenXRSpatialCapabilityConfigurationMicroQrCode : public OpenXRSpatialCapabilityConfigurationBaseHeader {
+	GDCLASS(OpenXRSpatialCapabilityConfigurationMicroQrCode, OpenXRSpatialCapabilityConfigurationBaseHeader);
+
+public:
+	virtual bool has_valid_configuration() const override;
+	virtual XrSpatialCapabilityConfigurationBaseHeaderEXT *get_configuration() override;
+
+	Vector<XrSpatialComponentTypeEXT> get_enabled_components() const { return enabled_components; }
+
+protected:
+	static void _bind_methods();
+
+private:
+	Vector<XrSpatialComponentTypeEXT> enabled_components;
+	XrSpatialCapabilityConfigurationMicroQrCodeEXT marker_config = { XR_TYPE_SPATIAL_CAPABILITY_CONFIGURATION_MICRO_QR_CODE_EXT, nullptr, XR_SPATIAL_CAPABILITY_MARKER_TRACKING_MICRO_QR_CODE_EXT, 0, nullptr };
+
+	PackedInt64Array _get_enabled_components() const;
+};
+
+// Aruco marker tracking capability configuration
+class OpenXRSpatialCapabilityConfigurationAruco : public OpenXRSpatialCapabilityConfigurationBaseHeader {
+	GDCLASS(OpenXRSpatialCapabilityConfigurationAruco, OpenXRSpatialCapabilityConfigurationBaseHeader);
+
+public:
+	enum ArucoDict {
+		ARUCO_DICT_4X4_50 = XR_SPATIAL_MARKER_ARUCO_DICT_4X4_50_EXT,
+		ARUCO_DICT_4X4_100 = XR_SPATIAL_MARKER_ARUCO_DICT_4X4_100_EXT,
+		ARUCO_DICT_4X4_250 = XR_SPATIAL_MARKER_ARUCO_DICT_4X4_250_EXT,
+		ARUCO_DICT_4X4_1000 = XR_SPATIAL_MARKER_ARUCO_DICT_4X4_1000_EXT,
+		ARUCO_DICT_5X5_50 = XR_SPATIAL_MARKER_ARUCO_DICT_5X5_50_EXT,
+		ARUCO_DICT_5X5_100 = XR_SPATIAL_MARKER_ARUCO_DICT_5X5_100_EXT,
+		ARUCO_DICT_5X5_250 = XR_SPATIAL_MARKER_ARUCO_DICT_5X5_250_EXT,
+		ARUCO_DICT_5X5_1000 = XR_SPATIAL_MARKER_ARUCO_DICT_5X5_1000_EXT,
+		ARUCO_DICT_6X6_50 = XR_SPATIAL_MARKER_ARUCO_DICT_6X6_50_EXT,
+		ARUCO_DICT_6X6_100 = XR_SPATIAL_MARKER_ARUCO_DICT_6X6_100_EXT,
+		ARUCO_DICT_6X6_250 = XR_SPATIAL_MARKER_ARUCO_DICT_6X6_250_EXT,
+		ARUCO_DICT_6X6_1000 = XR_SPATIAL_MARKER_ARUCO_DICT_6X6_1000_EXT,
+		ARUCO_DICT_7X7_50 = XR_SPATIAL_MARKER_ARUCO_DICT_7X7_50_EXT,
+		ARUCO_DICT_7X7_100 = XR_SPATIAL_MARKER_ARUCO_DICT_7X7_100_EXT,
+		ARUCO_DICT_7X7_250 = XR_SPATIAL_MARKER_ARUCO_DICT_7X7_250_EXT,
+		ARUCO_DICT_7X7_1000 = XR_SPATIAL_MARKER_ARUCO_DICT_7X7_1000_EXT,
+	};
+
+	virtual bool has_valid_configuration() const override;
+	virtual XrSpatialCapabilityConfigurationBaseHeaderEXT *get_configuration() override;
+
+	void set_aruco_dict(XrSpatialMarkerArucoDictEXT p_dict);
+	XrSpatialMarkerArucoDictEXT get_aruco_dict() const;
+
+	Vector<XrSpatialComponentTypeEXT> get_enabled_components() const { return enabled_components; }
+
+protected:
+	static void _bind_methods();
+
+private:
+	Vector<XrSpatialComponentTypeEXT> enabled_components;
+	XrSpatialCapabilityConfigurationArucoMarkerEXT marker_config = { XR_TYPE_SPATIAL_CAPABILITY_CONFIGURATION_ARUCO_MARKER_EXT, nullptr, XR_SPATIAL_CAPABILITY_MARKER_TRACKING_ARUCO_MARKER_EXT, 0, nullptr, XR_SPATIAL_MARKER_ARUCO_DICT_7X7_1000_EXT };
+
+	PackedInt64Array _get_enabled_components() const;
+
+	void _set_aruco_dict(ArucoDict p_dict);
+	ArucoDict _get_aruco_dict() const;
+};
+
+VARIANT_ENUM_CAST(OpenXRSpatialCapabilityConfigurationAruco::ArucoDict);
+
+// April tag marker tracking capability configuration
+class OpenXRSpatialCapabilityConfigurationAprilTag : public OpenXRSpatialCapabilityConfigurationBaseHeader {
+	GDCLASS(OpenXRSpatialCapabilityConfigurationAprilTag, OpenXRSpatialCapabilityConfigurationBaseHeader);
+
+public:
+	enum AprilTagDict {
+		APRIL_TAG_DICT_16H5 = XR_SPATIAL_MARKER_APRIL_TAG_DICT_16H5_EXT,
+		APRIL_TAG_DICT_25H9 = XR_SPATIAL_MARKER_APRIL_TAG_DICT_25H9_EXT,
+		APRIL_TAG_DICT_36H10 = XR_SPATIAL_MARKER_APRIL_TAG_DICT_36H10_EXT,
+		APRIL_TAG_DICT_36H11 = XR_SPATIAL_MARKER_APRIL_TAG_DICT_36H11_EXT,
+	};
+
+	virtual bool has_valid_configuration() const override;
+	virtual XrSpatialCapabilityConfigurationBaseHeaderEXT *get_configuration() override;
+
+	void set_april_dict(XrSpatialMarkerAprilTagDictEXT p_dict);
+	XrSpatialMarkerAprilTagDictEXT get_april_dict() const;
+
+	Vector<XrSpatialComponentTypeEXT> get_enabled_components() const { return enabled_components; }
+
+protected:
+	static void _bind_methods();
+
+private:
+	Vector<XrSpatialComponentTypeEXT> enabled_components;
+	XrSpatialCapabilityConfigurationAprilTagEXT marker_config = { XR_TYPE_SPATIAL_CAPABILITY_CONFIGURATION_APRIL_TAG_EXT, nullptr, XR_SPATIAL_CAPABILITY_MARKER_TRACKING_APRIL_TAG_EXT, 0, nullptr, XR_SPATIAL_MARKER_APRIL_TAG_DICT_36H11_EXT };
+
+	PackedInt64Array _get_enabled_components() const;
+
+	void _set_april_dict(AprilTagDict p_dict);
+	AprilTagDict _get_april_dict() const;
+};
+
+VARIANT_ENUM_CAST(OpenXRSpatialCapabilityConfigurationAprilTag::AprilTagDict);
+
+// Marker component data
+class OpenXRSpatialComponentMarkerList : public OpenXRSpatialComponentData {
+	GDCLASS(OpenXRSpatialComponentMarkerList, OpenXRSpatialComponentData);
+
+public:
+	enum MarkerType {
+		MARKER_TYPE_UNKNOWN,
+		MARKER_TYPE_QRCODE,
+		MARKER_TYPE_MICRO_QRCODE,
+		MARKER_TYPE_ARUCO,
+		MARKER_TYPE_APRIL_TAG,
+		MARKER_TYPE_MAX
+	};
+
+	virtual void set_capacity(uint32_t p_capacity) override;
+	virtual XrSpatialComponentTypeEXT get_component_type() const override;
+	virtual void *get_structure_data(void *p_next) override;
+
+	MarkerType get_marker_type(int64_t p_index) const;
+	uint32_t get_marker_id(int64_t p_index) const;
+	Variant get_marker_data(RID p_snapshot, int64_t p_index) const;
+
+protected:
+	static void _bind_methods();
+
+private:
+	Vector<XrSpatialMarkerDataEXT> marker_data;
+
+	XrSpatialComponentMarkerListEXT marker_list = { XR_TYPE_SPATIAL_COMPONENT_MARKER_LIST_EXT, nullptr, 0, nullptr };
+};
+
+VARIANT_ENUM_CAST(OpenXRSpatialComponentMarkerList::MarkerType);
+
+// Marker tracker
+class OpenXRMarkerTracker : public OpenXRSpatialEntityTracker {
+	GDCLASS(OpenXRMarkerTracker, OpenXRSpatialEntityTracker);
+
+public:
+	void set_bounds_size(const Vector2 &p_bounds_size);
+	Vector2 get_bounds_size() const;
+
+	void set_marker_type(OpenXRSpatialComponentMarkerList::MarkerType p_marker_type);
+	OpenXRSpatialComponentMarkerList::MarkerType get_marker_type() const;
+
+	void set_marker_id(uint32_t p_id);
+	uint32_t get_marker_id() const;
+
+	void set_marker_data(const Variant &p_data);
+	Variant get_marker_data() const;
+
+protected:
+	static void _bind_methods();
+
+private:
+	Vector2 bounds_size;
+
+	OpenXRSpatialComponentMarkerList::MarkerType marker_type = OpenXRSpatialComponentMarkerList::MarkerType::MARKER_TYPE_UNKNOWN;
+	uint32_t marker_id = 0;
+	Variant marker_data;
+};
+
+// Marker tracking logic
+class OpenXRSpatialMarkerTrackingCapability : public OpenXRExtensionWrapper {
+	GDCLASS(OpenXRSpatialMarkerTrackingCapability, OpenXRExtensionWrapper);
+
+protected:
+	static void _bind_methods();
+
+public:
+	static OpenXRSpatialMarkerTrackingCapability *get_singleton();
+
+	OpenXRSpatialMarkerTrackingCapability();
+	virtual ~OpenXRSpatialMarkerTrackingCapability() override;
+
+	virtual HashMap<String, bool *> get_requested_extensions() override;
+
+	virtual void on_session_created(const XrSession p_session) override;
+	virtual void on_session_destroyed() override;
+
+	virtual void on_process() override;
+
+	bool is_qrcode_supported();
+	bool is_micro_qrcode_supported();
+	bool is_aruco_supported();
+	bool is_april_tag_supported();
+
+private:
+	static OpenXRSpatialMarkerTrackingCapability *singleton;
+	bool spatial_marker_tracking_ext = false;
+
+	RID spatial_context;
+	bool need_discovery = false;
+	int discovery_cooldown = 0;
+	Ref<OpenXRFutureResult> discovery_query_result;
+
+	Ref<OpenXRSpatialCapabilityConfigurationQrCode> qrcode_configuration;
+	Ref<OpenXRSpatialCapabilityConfigurationMicroQrCode> micro_qrcode_configuration;
+	Ref<OpenXRSpatialCapabilityConfigurationAruco> aruco_configuration;
+	Ref<OpenXRSpatialCapabilityConfigurationAprilTag> april_tag_configuration;
+
+	// Discovery logic
+	Ref<OpenXRFutureResult> _create_spatial_context();
+	void _on_spatial_context_created(RID p_spatial_context);
+
+	void _on_spatial_discovery_recommended(RID p_spatial_context);
+
+	Ref<OpenXRFutureResult> _start_entity_discovery();
+	void _process_snapshot(RID p_snapshot, bool p_is_discovery);
+
+	// Trackers
+	HashMap<XrSpatialEntityIdEXT, Ref<OpenXRMarkerTracker>> marker_trackers;
+};

+ 879 - 0
modules/openxr/extensions/spatial_entities/openxr_spatial_plane_tracking.cpp

@@ -0,0 +1,879 @@
+/**************************************************************************/
+/*  openxr_spatial_plane_tracking.cpp                                     */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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 "openxr_spatial_plane_tracking.h"
+
+#include "../../openxr_api.h"
+#include "core/config/project_settings.h"
+#include "scene/resources/3d/box_shape_3d.h"
+#include "scene/resources/3d/concave_polygon_shape_3d.h"
+#include "scene/resources/3d/primitive_meshes.h"
+#include "servers/xr_server.h"
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialCapabilityConfigurationPlaneTracking
+
+void OpenXRSpatialCapabilityConfigurationPlaneTracking::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("supports_mesh_2d"), &OpenXRSpatialCapabilityConfigurationPlaneTracking::get_supports_mesh_2d);
+	ClassDB::bind_method(D_METHOD("supports_polygons"), &OpenXRSpatialCapabilityConfigurationPlaneTracking::get_supports_polygons);
+	ClassDB::bind_method(D_METHOD("supports_labels"), &OpenXRSpatialCapabilityConfigurationPlaneTracking::get_supports_labels);
+
+	ClassDB::bind_method(D_METHOD("get_enabled_components"), &OpenXRSpatialCapabilityConfigurationPlaneTracking::_get_enabled_components);
+}
+
+bool OpenXRSpatialCapabilityConfigurationPlaneTracking::has_valid_configuration() const {
+	OpenXRSpatialPlaneTrackingCapability *capability = OpenXRSpatialPlaneTrackingCapability::get_singleton();
+	ERR_FAIL_NULL_V(capability, false);
+
+	return capability->is_supported();
+}
+
+XrSpatialCapabilityConfigurationBaseHeaderEXT *OpenXRSpatialCapabilityConfigurationPlaneTracking::get_configuration() {
+	OpenXRSpatialPlaneTrackingCapability *capability = OpenXRSpatialPlaneTrackingCapability::get_singleton();
+	ERR_FAIL_NULL_V(capability, nullptr);
+
+	if (capability->is_supported()) {
+		OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+		ERR_FAIL_NULL_V(se_extension, nullptr);
+
+		// Guaranteed components:
+		plane_enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D_EXT);
+		plane_enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_PLANE_ALIGNMENT_EXT);
+
+		// Optional components:
+		if (get_supports_mesh_2d()) {
+			plane_enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_MESH_2D_EXT);
+		} else if (get_supports_polygons()) {
+			plane_enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_POLYGON_2D_EXT);
+		}
+		if (get_supports_labels()) {
+			plane_enabled_components.push_back(XR_SPATIAL_COMPONENT_TYPE_PLANE_SEMANTIC_LABEL_EXT);
+		}
+
+		// Set up our enabled components.
+		plane_config.enabledComponentCount = plane_enabled_components.size();
+		plane_config.enabledComponents = plane_enabled_components.ptr();
+
+		// and return this.
+		return (XrSpatialCapabilityConfigurationBaseHeaderEXT *)&plane_config;
+	}
+
+	return nullptr;
+}
+
+bool OpenXRSpatialCapabilityConfigurationPlaneTracking::get_supports_mesh_2d() {
+	if (supports_mesh_2d == -1) {
+		OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+		ERR_FAIL_NULL_V(se_extension, false);
+
+		supports_mesh_2d = se_extension->supports_component_type(XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT, XR_SPATIAL_COMPONENT_TYPE_MESH_2D_EXT) ? 1 : 0;
+	}
+
+	return supports_mesh_2d == 1;
+}
+
+bool OpenXRSpatialCapabilityConfigurationPlaneTracking::get_supports_polygons() {
+	if (supports_polygons == -1) {
+		OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+		ERR_FAIL_NULL_V(se_extension, false);
+
+		supports_polygons = se_extension->supports_component_type(XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT, XR_SPATIAL_COMPONENT_TYPE_POLYGON_2D_EXT) ? 1 : 0;
+	}
+
+	return supports_polygons == 1;
+}
+
+bool OpenXRSpatialCapabilityConfigurationPlaneTracking::get_supports_labels() {
+	if (supports_labels == -1) {
+		OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+		ERR_FAIL_NULL_V(se_extension, false);
+
+		supports_labels = se_extension->supports_component_type(XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT, XR_SPATIAL_COMPONENT_TYPE_PLANE_SEMANTIC_LABEL_EXT) ? 1 : 0;
+	}
+
+	return supports_labels == 1;
+}
+
+PackedInt64Array OpenXRSpatialCapabilityConfigurationPlaneTracking::_get_enabled_components() const {
+	PackedInt64Array components;
+
+	for (const XrSpatialComponentTypeEXT &component_type : plane_enabled_components) {
+		components.push_back((int64_t)component_type);
+	}
+
+	return components;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialComponentPlaneAlignmentList
+
+void OpenXRSpatialComponentPlaneAlignmentList::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_plane_alignment", "index"), &OpenXRSpatialComponentPlaneAlignmentList::_get_plane_alignment);
+
+	BIND_ENUM_CONSTANT(PLANE_ALIGNMENT_HORIZONTAL_UPWARD);
+	BIND_ENUM_CONSTANT(PLANE_ALIGNMENT_HORIZONTAL_DOWNWARD);
+	BIND_ENUM_CONSTANT(PLANE_ALIGNMENT_VERTICAL);
+	BIND_ENUM_CONSTANT(PLANE_ALIGNMENT_ARBITRARY);
+}
+
+void OpenXRSpatialComponentPlaneAlignmentList::set_capacity(uint32_t p_capacity) {
+	plane_alignment_data.resize(p_capacity);
+
+	plane_alignment_list.planeAlignmentCount = uint32_t(plane_alignment_data.size());
+	plane_alignment_list.planeAlignments = plane_alignment_data.ptrw();
+}
+
+XrSpatialComponentTypeEXT OpenXRSpatialComponentPlaneAlignmentList::get_component_type() const {
+	return XR_SPATIAL_COMPONENT_TYPE_PLANE_ALIGNMENT_EXT;
+}
+
+void *OpenXRSpatialComponentPlaneAlignmentList::get_structure_data(void *p_next) {
+	plane_alignment_list.next = p_next;
+	return &plane_alignment_list;
+}
+
+XrSpatialPlaneAlignmentEXT OpenXRSpatialComponentPlaneAlignmentList::get_plane_alignment(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, plane_alignment_data.size(), XR_SPATIAL_PLANE_ALIGNMENT_MAX_ENUM_EXT);
+
+	return plane_alignment_data[p_index];
+}
+
+OpenXRSpatialComponentPlaneAlignmentList::PlaneAlignment OpenXRSpatialComponentPlaneAlignmentList::_get_plane_alignment(int64_t p_index) const {
+	return (PlaneAlignment)get_plane_alignment(p_index);
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Spatial component polygon2d list
+
+void OpenXRSpatialComponentPolygon2DList::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_transform", "index"), &OpenXRSpatialComponentPolygon2DList::get_transform);
+	ClassDB::bind_method(D_METHOD("get_vertices", "snapshot", "index"), &OpenXRSpatialComponentPolygon2DList::get_vertices);
+}
+
+void OpenXRSpatialComponentPolygon2DList::set_capacity(uint32_t p_capacity) {
+	polygon2d_data.resize(p_capacity);
+
+	polygon2d_list.polygonCount = uint32_t(polygon2d_data.size());
+	polygon2d_list.polygons = polygon2d_data.ptrw();
+}
+
+XrSpatialComponentTypeEXT OpenXRSpatialComponentPolygon2DList::get_component_type() const {
+	return XR_SPATIAL_COMPONENT_TYPE_POLYGON_2D_EXT;
+}
+
+void *OpenXRSpatialComponentPolygon2DList::get_structure_data(void *p_next) {
+	polygon2d_list.next = p_next;
+	return &polygon2d_list;
+}
+
+Transform3D OpenXRSpatialComponentPolygon2DList::get_transform(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, polygon2d_data.size(), Transform3D());
+
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL_V(openxr_api, Transform3D());
+
+	return openxr_api->transform_from_pose(polygon2d_data[p_index].origin);
+}
+
+PackedVector2Array OpenXRSpatialComponentPolygon2DList::get_vertices(RID p_snapshot, int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, polygon2d_data.size(), PackedVector2Array());
+
+	const XrSpatialBufferEXT &buffer = polygon2d_data[p_index].vertexBuffer;
+	if (buffer.bufferId == XR_NULL_SPATIAL_BUFFER_ID_EXT) {
+		// We don't have data (yet).
+		return PackedVector2Array();
+	}
+
+	ERR_FAIL_COND_V(buffer.bufferType != XR_SPATIAL_BUFFER_TYPE_VECTOR2F_EXT, PackedVector2Array());
+
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, PackedVector2Array());
+
+	return se_extension->get_vector2_buffer(p_snapshot, buffer.bufferId);
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialComponentPlaneSemanticLabelList
+
+void OpenXRSpatialComponentPlaneSemanticLabelList::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_plane_semantic_label", "index"), &OpenXRSpatialComponentPlaneSemanticLabelList::_get_plane_semantic_label);
+
+	BIND_ENUM_CONSTANT(PLANE_SEMANTIC_LABEL_UNCATEGORIZED);
+	BIND_ENUM_CONSTANT(PLANE_SEMANTIC_LABEL_FLOOR);
+	BIND_ENUM_CONSTANT(PLANE_SEMANTIC_LABEL_WALL);
+	BIND_ENUM_CONSTANT(PLANE_SEMANTIC_LABEL_CEILING);
+	BIND_ENUM_CONSTANT(PLANE_SEMANTIC_LABEL_TABLE);
+}
+
+void OpenXRSpatialComponentPlaneSemanticLabelList::set_capacity(uint32_t p_capacity) {
+	plane_semantic_label_data.resize(p_capacity);
+
+	plane_semantic_label_list.semanticLabelCount = uint32_t(plane_semantic_label_data.size());
+	plane_semantic_label_list.semanticLabels = plane_semantic_label_data.ptrw();
+}
+
+XrSpatialComponentTypeEXT OpenXRSpatialComponentPlaneSemanticLabelList::get_component_type() const {
+	return XR_SPATIAL_COMPONENT_TYPE_PLANE_SEMANTIC_LABEL_EXT;
+}
+
+void *OpenXRSpatialComponentPlaneSemanticLabelList::get_structure_data(void *p_next) {
+	plane_semantic_label_list.next = p_next;
+	return &plane_semantic_label_list;
+}
+
+XrSpatialPlaneSemanticLabelEXT OpenXRSpatialComponentPlaneSemanticLabelList::get_plane_semantic_label(int64_t p_index) const {
+	ERR_FAIL_INDEX_V(p_index, plane_semantic_label_data.size(), XR_SPATIAL_PLANE_SEMANTIC_LABEL_MAX_ENUM_EXT);
+
+	return plane_semantic_label_data[p_index];
+}
+
+OpenXRSpatialComponentPlaneSemanticLabelList::PlaneSemanticLabel OpenXRSpatialComponentPlaneSemanticLabelList::_get_plane_semantic_label(int64_t p_index) const {
+	return (PlaneSemanticLabel)get_plane_semantic_label(p_index);
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRPlaneTracker
+
+void OpenXRPlaneTracker::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("set_bounds_size", "bounds_size"), &OpenXRPlaneTracker::set_bounds_size);
+	ClassDB::bind_method(D_METHOD("get_bounds_size"), &OpenXRPlaneTracker::get_bounds_size);
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "bounds_size"), "set_bounds_size", "get_bounds_size");
+
+	ClassDB::bind_method(D_METHOD("set_plane_alignment", "plane_alignment"), &OpenXRPlaneTracker::set_plane_alignment);
+	ClassDB::bind_method(D_METHOD("get_plane_alignment"), &OpenXRPlaneTracker::get_plane_alignment);
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "plane_alignment"), "set_plane_alignment", "get_plane_alignment");
+
+	ClassDB::bind_method(D_METHOD("set_plane_label", "plane_label"), &OpenXRPlaneTracker::set_plane_label);
+	ClassDB::bind_method(D_METHOD("get_plane_label"), &OpenXRPlaneTracker::get_plane_label);
+	ADD_PROPERTY(PropertyInfo(Variant::STRING, "plane_label"), "set_plane_label", "get_plane_label");
+
+	ClassDB::bind_method(D_METHOD("set_mesh_data", "origin", "vertices", "indices"), &OpenXRPlaneTracker::set_mesh_data, DEFVAL(PackedInt32Array()));
+	ClassDB::bind_method(D_METHOD("clear_mesh_data"), &OpenXRPlaneTracker::clear_mesh_data);
+
+	ClassDB::bind_method(D_METHOD("get_mesh_offset"), &OpenXRPlaneTracker::get_mesh_offset);
+	ClassDB::bind_method(D_METHOD("get_mesh"), &OpenXRPlaneTracker::get_mesh);
+	ClassDB::bind_method(D_METHOD("get_shape", "thickness"), &OpenXRPlaneTracker::get_shape, DEFVAL(0.01));
+
+	ADD_SIGNAL(MethodInfo("mesh_changed"));
+}
+
+void OpenXRPlaneTracker::set_bounds_size(const Vector2 &p_bounds_size) {
+	if (Math::abs(bounds_size.x - p_bounds_size.x) > 0.001 || Math::abs(bounds_size.y - p_bounds_size.y) > 0.001) {
+		bounds_size = p_bounds_size;
+
+		if (!mesh.has_mesh_data) {
+			// Bounds changing only effects mesh data if we don't have polygon data.
+			clear_mesh_data();
+			emit_signal(SNAME("mesh_changed"));
+		}
+	}
+}
+
+Vector2 OpenXRPlaneTracker::get_bounds_size() const {
+	return bounds_size;
+}
+
+void OpenXRPlaneTracker::set_plane_alignment(OpenXRSpatialComponentPlaneAlignmentList::PlaneAlignment p_plane_alignment) {
+	if (plane_alignment != p_plane_alignment) {
+		plane_alignment = p_plane_alignment;
+	}
+}
+
+OpenXRSpatialComponentPlaneAlignmentList::PlaneAlignment OpenXRPlaneTracker::get_plane_alignment() const {
+	return plane_alignment;
+}
+
+void OpenXRPlaneTracker::set_plane_label(const String &p_plane_label) {
+	if (plane_label != p_plane_label) {
+		plane_label = p_plane_label;
+
+		// Also copy to description, should do something nicer here.
+		set_tracker_desc(plane_label);
+	}
+}
+
+String OpenXRPlaneTracker::get_plane_label() const {
+	return plane_label;
+}
+
+void OpenXRPlaneTracker::set_mesh_data(const Transform3D &p_origin, const PackedVector2Array &p_vertices, const PackedInt32Array &p_indices) {
+	if (p_vertices.size() < 3) {
+		if (mesh.has_mesh_data) {
+			clear_mesh_data();
+			emit_signal(SNAME("mesh_changed"));
+		}
+	} else {
+		bool has_changed = !mesh.has_mesh_data;
+
+		mesh.has_mesh_data = true;
+		mesh.origin = p_origin;
+
+		if (mesh.vertices.size() != p_vertices.size()) {
+			has_changed = true;
+		} else {
+			// Compare the vertices with a bit of margin, we ignore small jittering on vertices.
+			for (uint32_t i = 0; i < p_vertices.size() && !has_changed; i++) {
+				const Vector2 &a = p_vertices[i];
+				const Vector2 &b = mesh.vertices[i];
+				has_changed = (Math::abs(a.x - b.x) > 0.001) || (Math::abs(a.y - b.y) > 0.001);
+			}
+		}
+		if (has_changed) {
+			mesh.vertices = p_vertices;
+		}
+
+		// Q: Should we keep our indices list empty if we get polygon data
+		// and create different meshes/collision shapes as a result?
+		if (p_indices.is_empty()) {
+			// Assume polygon, turn into triangle strip...
+			int count = (p_vertices.size() - 2) * 3;
+
+			// If our vertices haven't changed and our indices are already the correct size,
+			// assume we don't need to rerun this.
+			if (has_changed || mesh.indices.size() != count) {
+				has_changed = true;
+
+				int offset = 1;
+				mesh.indices.resize(count);
+				int32_t *idx = mesh.indices.ptrw();
+				for (int i = 0; i < count; i += 3) {
+					idx[i + 0] = 0;
+					idx[i + 2] = offset++;
+					idx[i + 1] = offset;
+				}
+			}
+		} else {
+			if (mesh.indices.size() != p_indices.size()) {
+				has_changed = true;
+			} else {
+				for (uint32_t i = 0; i < p_indices.size() && !has_changed; i++) {
+					has_changed = mesh.indices[i] != p_indices[i];
+				}
+			}
+			if (has_changed) {
+				mesh.indices = p_indices;
+			}
+		}
+
+		if (has_changed) {
+			mesh.mesh.unref();
+			mesh.shape3d.unref();
+
+			emit_signal(SNAME("mesh_changed"));
+		}
+	}
+}
+
+void OpenXRPlaneTracker::clear_mesh_data() {
+	mesh.mesh.unref();
+	mesh.shape3d.unref();
+
+	if (mesh.has_mesh_data) {
+		mesh.has_mesh_data = false;
+		mesh.origin = Transform3D();
+		mesh.vertices.clear();
+		mesh.indices.clear();
+
+		emit_signal(SNAME("mesh_changed"));
+	}
+}
+
+Transform3D OpenXRPlaneTracker::get_mesh_offset() const {
+	Transform3D offset;
+
+	if (mesh.has_mesh_data) {
+		offset = mesh.origin;
+
+		Ref<XRPose> pose = get_pose(SNAME("default"));
+		if (pose.is_valid()) {
+			// Q is this offset * transform.inverse?
+			offset = pose->get_transform().inverse() * offset;
+		}
+
+		// Reference frame will already be applied to pose used on our XRNode3D but we do need to apply our scale
+		XRServer *xr_server = XRServer::get_singleton();
+		if (xr_server) {
+			offset.origin *= xr_server->get_world_scale();
+		}
+	}
+
+	return offset;
+}
+
+Ref<Mesh> OpenXRPlaneTracker::get_mesh() {
+	// We've already created this? Just return it!
+	if (mesh.mesh.is_valid()) {
+		return mesh.mesh;
+	}
+
+	if (mesh.has_mesh_data) {
+		Ref<ArrayMesh> array_mesh;
+		Array arr;
+
+		// We need our vertices as Vector3
+		PackedVector3Array vertices;
+		vertices.resize(mesh.vertices.size());
+		const Vector2 *read = mesh.vertices.ptr();
+		Vector3 *write = vertices.ptrw();
+		for (int v = 0; v < mesh.vertices.size(); v++) {
+			write[v] = Vector3(read[v].x, read[v].y, 0.0);
+		}
+
+		// Build our array with data.
+		arr.resize(RS::ARRAY_MAX);
+		arr[RS::ARRAY_VERTEX] = vertices;
+		arr[RS::ARRAY_INDEX] = mesh.indices;
+
+		// Create our array mesh.
+		array_mesh.instantiate();
+		array_mesh->add_surface_from_arrays(Mesh::PrimitiveType::PRIMITIVE_TRIANGLES, arr);
+
+		// Cache this.
+		mesh.mesh = array_mesh;
+	} else if (bounds_size.x > 0.0 && bounds_size.y > 0.0) {
+		// We can use a plane mesh here.
+		Ref<PlaneMesh> plane_mesh;
+
+		plane_mesh.instantiate();
+		plane_mesh->set_orientation(PlaneMesh::Orientation::FACE_Z);
+		plane_mesh->set_size(bounds_size);
+
+		// Cache this.
+		mesh.mesh = plane_mesh;
+	} else {
+		print_verbose("OpenXR: Can't create mesh for plane, no data.");
+	}
+	return mesh.mesh;
+}
+
+Ref<Shape3D> OpenXRPlaneTracker::get_shape(real_t p_thickness) {
+	// We've already created this? Just return it!
+	if (mesh.shape3d.is_valid()) {
+		return mesh.shape3d;
+	}
+
+	if (mesh.has_mesh_data) {
+		Ref<ConcavePolygonShape3D> shape;
+		Vector<Vector3> faces;
+
+		// Get some direct access to our data.
+		int isize = mesh.indices.size();
+		const Vector2 *vr = mesh.vertices.ptr();
+		const int32_t *ir = mesh.indices.ptr();
+
+		// Find our edges.
+		HashMap<Edge, int, Edge> edge_counts;
+		for (int i = 0; i < isize; i += 3) {
+			for (int j = 0; j < 3; j++) {
+				Edge e(ir[i + j], ir[i + ((j + 1) % 3)]);
+				edge_counts[e]++;
+			}
+		}
+
+		// Find our outer edges.
+		thread_local LocalVector<Edge> outer_edges;
+		outer_edges.clear();
+		for (const KeyValue<Edge, int> &e : edge_counts) {
+			if (e.value > 1) {
+				outer_edges.push_back(e.key);
+			}
+		}
+
+		// Make space for these.
+		faces.resize(2 * isize + 6 * outer_edges.size());
+		Vector3 *write = faces.ptrw();
+
+		// Add top and bottom.
+		for (int i = 0; i < isize; i += 3) {
+			Vector3 a = Vector3(vr[ir[i]].x, vr[ir[i]].y, 0.0);
+			Vector3 b = Vector3(vr[ir[i + 1]].x, vr[ir[i + 1]].y, 0.0);
+			Vector3 c = Vector3(vr[ir[i + 2]].x, vr[ir[i + 2]].y, 0.0);
+
+			*write++ = a;
+			*write++ = b;
+			*write++ = c;
+
+			a.z = -p_thickness;
+			b.z = -p_thickness;
+			c.z = -p_thickness;
+
+			*write++ = a;
+			*write++ = c;
+			*write++ = b;
+		}
+
+		// Add outer edges.
+		for (const Edge &edge : outer_edges) {
+			Vector3 a = Vector3(vr[edge.a].x, vr[edge.a].y, 0.0);
+			Vector3 b = Vector3(vr[edge.b].x, vr[edge.b].y, 0.0);
+			Vector3 c = b + Vector3(0.0, 0.0, -p_thickness);
+			Vector3 d = a + Vector3(0.0, 0.0, -p_thickness);
+
+			*write++ = a;
+			*write++ = b;
+			*write++ = c;
+
+			*write++ = a;
+			*write++ = c;
+			*write++ = d;
+		}
+
+		// Create our shape.
+		shape.instantiate();
+		shape->set_faces(faces);
+
+		mesh.shape3d = shape;
+	} else if (bounds_size.x > 0.0 && bounds_size.y > 0.0) {
+		// We can use a box shape here
+		Ref<BoxShape3D> box_shape;
+		box_shape.instantiate();
+		box_shape->set_size(Vector3(bounds_size.x, bounds_size.y, p_thickness));
+
+		mesh.shape3d = box_shape;
+	}
+
+	return mesh.shape3d;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRSpatialPlaneTrackingCapability
+
+OpenXRSpatialPlaneTrackingCapability *OpenXRSpatialPlaneTrackingCapability::singleton = nullptr;
+
+OpenXRSpatialPlaneTrackingCapability *OpenXRSpatialPlaneTrackingCapability::get_singleton() {
+	return singleton;
+}
+
+OpenXRSpatialPlaneTrackingCapability::OpenXRSpatialPlaneTrackingCapability() {
+	singleton = this;
+}
+
+OpenXRSpatialPlaneTrackingCapability::~OpenXRSpatialPlaneTrackingCapability() {
+	singleton = nullptr;
+}
+
+void OpenXRSpatialPlaneTrackingCapability::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("is_supported"), &OpenXRSpatialPlaneTrackingCapability::is_supported);
+}
+
+HashMap<String, bool *> OpenXRSpatialPlaneTrackingCapability::get_requested_extensions() {
+	HashMap<String, bool *> request_extensions;
+
+	if (GLOBAL_GET_CACHED(bool, "xr/openxr/extensions/spatial_entity/enabled") && GLOBAL_GET_CACHED(bool, "xr/openxr/extensions/spatial_entity/enable_plane_tracking")) {
+		request_extensions[XR_EXT_SPATIAL_PLANE_TRACKING_EXTENSION_NAME] = &spatial_plane_tracking_ext;
+	}
+
+	return request_extensions;
+}
+
+void OpenXRSpatialPlaneTrackingCapability::on_session_created(const XrSession p_session) {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL(se_extension);
+
+	if (!spatial_plane_tracking_ext) {
+		return;
+	}
+
+	spatial_plane_tracking_supported = se_extension->supports_capability(XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT);
+	if (!spatial_plane_tracking_supported) {
+		// Supported by XR runtime but not by device? We're done.
+		return;
+	}
+
+	se_extension->connect(SNAME("spatial_discovery_recommended"), callable_mp(this, &OpenXRSpatialPlaneTrackingCapability::_on_spatial_discovery_recommended));
+
+	if (GLOBAL_GET_CACHED(bool, "xr/openxr/extensions/spatial_entity/enable_builtin_plane_detection")) {
+		// Start by creating our spatial context
+		_create_spatial_context();
+	}
+}
+
+void OpenXRSpatialPlaneTrackingCapability::on_session_destroyed() {
+	if (!spatial_plane_tracking_supported) {
+		return;
+	}
+	spatial_plane_tracking_supported = false;
+
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL(se_extension);
+	XRServer *xr_server = XRServer::get_singleton();
+	ERR_FAIL_NULL(xr_server);
+
+	// Free and unregister our anchors
+	for (const KeyValue<XrSpatialEntityIdEXT, Ref<OpenXRPlaneTracker>> &plane_tracker : plane_trackers) {
+		xr_server->remove_tracker(plane_tracker.value);
+	}
+	plane_trackers.clear();
+
+	// Free our spatial context
+	if (spatial_context.is_valid()) {
+		se_extension->free_spatial_context(spatial_context);
+		spatial_context = RID();
+	}
+
+	se_extension->disconnect(SNAME("spatial_discovery_recommended"), callable_mp(this, &OpenXRSpatialPlaneTrackingCapability::_on_spatial_discovery_recommended));
+}
+
+void OpenXRSpatialPlaneTrackingCapability::on_process() {
+	if (!spatial_context.is_valid()) {
+		return;
+	}
+
+	// Protection against plane discovery happening too often.
+	if (discovery_cooldown > 0) {
+		discovery_cooldown--;
+	}
+
+	// Check if we need to start our discovery.
+	if (need_discovery && discovery_cooldown == 0 && !discovery_query_result.is_valid()) {
+		need_discovery = false;
+		discovery_cooldown = 60; // Set our cooldown to 60 frames, it doesn't need to be an exact science.
+
+		_start_entity_discovery();
+	}
+}
+
+bool OpenXRSpatialPlaneTrackingCapability::is_supported() {
+	return spatial_plane_tracking_supported;
+}
+
+////////////////////////////////////////////////////////////////////////////
+// Discovery logic
+Ref<OpenXRFutureResult> OpenXRSpatialPlaneTrackingCapability::_create_spatial_context() {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, nullptr);
+
+	TypedArray<OpenXRSpatialCapabilityConfigurationBaseHeader> capability_configurations;
+
+	// Create our configuration objects.
+	plane_configuration.instantiate();
+	capability_configurations.push_back(plane_configuration);
+
+	return se_extension->create_spatial_context(capability_configurations, nullptr, callable_mp(this, &OpenXRSpatialPlaneTrackingCapability::_on_spatial_context_created));
+}
+
+void OpenXRSpatialPlaneTrackingCapability::_on_spatial_context_created(RID p_spatial_context) {
+	spatial_context = p_spatial_context;
+	need_discovery = true;
+}
+
+void OpenXRSpatialPlaneTrackingCapability::_on_spatial_discovery_recommended(RID p_spatial_context) {
+	if (p_spatial_context == spatial_context) {
+		// Trigger new discovery.
+		need_discovery = true;
+	}
+}
+
+Ref<OpenXRFutureResult> OpenXRSpatialPlaneTrackingCapability::_start_entity_discovery() {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL_V(se_extension, nullptr);
+
+	// Already running or ran discovery, cancel/clean up.
+	if (discovery_query_result.is_valid()) {
+		WARN_PRINT("OpenXR: Starting new discovery before previous discovery has been processed!");
+		discovery_query_result->cancel_future();
+		discovery_query_result.unref();
+	}
+
+	// Start our new snapshot.
+	discovery_query_result = se_extension->discover_spatial_entities(spatial_context, plane_configuration->get_enabled_components(), nullptr, callable_mp(this, &OpenXRSpatialPlaneTrackingCapability::_process_snapshot));
+
+	return discovery_query_result;
+}
+
+void OpenXRSpatialPlaneTrackingCapability::_process_snapshot(RID p_snapshot) {
+	OpenXRSpatialEntityExtension *se_extension = OpenXRSpatialEntityExtension::get_singleton();
+	ERR_FAIL_NULL(se_extension);
+	XRServer *xr_server = XRServer::get_singleton();
+	ERR_FAIL_NULL(xr_server);
+	OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+	ERR_FAIL_NULL(openxr_api);
+
+	// Make a copy of the planes we have right now, so we know which ones to clean up.
+	LocalVector<XrSpatialEntityIdEXT> current_planes;
+	current_planes.resize(plane_trackers.size());
+	int p = 0;
+	for (const KeyValue<XrSpatialEntityIdEXT, Ref<OpenXRPlaneTracker>> &plane : plane_trackers) {
+		current_planes[p++] = plane.key;
+	}
+
+	// Build our component data
+	TypedArray<OpenXRSpatialComponentData> component_data;
+
+	// We always need a query result data object
+	Ref<OpenXRSpatialQueryResultData> query_result_data;
+	query_result_data.instantiate();
+	component_data.push_back(query_result_data);
+
+	// Add bounded2D
+	Ref<OpenXRSpatialComponentBounded2DList> bounded2d_list;
+	bounded2d_list.instantiate();
+	component_data.push_back(bounded2d_list);
+
+	// Plane alignment list
+	Ref<OpenXRSpatialComponentPlaneAlignmentList> alignment_list;
+	alignment_list.instantiate();
+	component_data.push_back(alignment_list);
+
+	Ref<OpenXRSpatialComponentMesh2DList> mesh2d_list;
+	Ref<OpenXRSpatialComponentPolygon2DList> poly2d_list;
+	if (plane_configuration->get_supports_mesh_2d()) {
+		mesh2d_list.instantiate();
+		component_data.push_back(mesh2d_list);
+	} else if (plane_configuration->get_supports_polygons()) {
+		poly2d_list.instantiate();
+		component_data.push_back(poly2d_list);
+	}
+
+	// Plane semantic label
+	Ref<OpenXRSpatialComponentPlaneSemanticLabelList> label_list;
+	if (plane_configuration->get_supports_labels()) {
+		label_list.instantiate();
+		component_data.push_back(label_list);
+	}
+
+	if (se_extension->query_snapshot(p_snapshot, component_data, nullptr)) {
+		// Now loop through our data and update our anchors.
+		// Q we're assuming entity ID, size and state size are equal, is there ever a situation where they would not be?
+		int64_t size = query_result_data->get_capacity();
+		for (int64_t i = 0; i < size; i++) {
+			XrSpatialEntityIdEXT entity_id = query_result_data->get_entity_id(i);
+			XrSpatialEntityTrackingStateEXT entity_state = query_result_data->get_entity_state(i);
+
+			// Erase it from our current planes (if we have it, else this is ignored).
+			current_planes.erase(entity_id);
+
+			if (entity_state == XR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED_EXT) {
+				// We should only get this status on updates as a prelude to needing to remove this marker.
+				// So we just update the status.
+				if (plane_trackers.has(entity_id)) {
+					Ref<OpenXRPlaneTracker> plane_tracker = plane_trackers[entity_id];
+					plane_tracker->invalidate_pose(SNAME("default"));
+					plane_tracker->set_spatial_tracking_state(XR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED_EXT);
+				}
+			} else {
+				// Process our entity
+				bool add_to_xr_server = false;
+				Ref<OpenXRPlaneTracker> plane_tracker;
+
+				if (plane_trackers.has(entity_id)) {
+					// We know about this one already
+					plane_tracker = plane_trackers[entity_id];
+				} else {
+					// Create a new anchor
+					plane_tracker.instantiate();
+					plane_tracker->set_entity(se_extension->make_spatial_entity(se_extension->get_spatial_snapshot_context(p_snapshot), entity_id));
+					plane_trackers[entity_id] = plane_tracker;
+
+					add_to_xr_server = true;
+				}
+
+				// Handle component data
+				if (entity_state == XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT) {
+					plane_tracker->invalidate_pose(SNAME("default"));
+					plane_tracker->set_spatial_tracking_state(XR_SPATIAL_ENTITY_TRACKING_STATE_PAUSED_EXT);
+
+					// No further component data will be valid in this state, we need to ignore it!
+				} else if (entity_state == XR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING_EXT) {
+					Transform3D transform = bounded2d_list->get_center_pose(i);
+					plane_tracker->set_pose(SNAME("default"), transform, Vector3(), Vector3());
+					plane_tracker->set_spatial_tracking_state(XR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING_EXT);
+
+					// Process our component data.
+					plane_tracker->set_bounds_size(bounded2d_list->get_size(i));
+					plane_tracker->set_plane_alignment((OpenXRSpatialComponentPlaneAlignmentList::PlaneAlignment)alignment_list->get_plane_alignment(i));
+
+					if (mesh2d_list.is_valid()) {
+						plane_tracker->set_mesh_data(mesh2d_list->get_transform(i), mesh2d_list->get_vertices(p_snapshot, i), mesh2d_list->get_indices(p_snapshot, i));
+					} else if (poly2d_list.is_valid()) {
+						plane_tracker->set_mesh_data(poly2d_list->get_transform(i), poly2d_list->get_vertices(p_snapshot, i));
+					} else {
+						// Just in case we set this before.
+						plane_tracker->clear_mesh_data();
+					}
+
+					if (label_list.is_valid()) {
+						switch (label_list->get_plane_semantic_label(i)) {
+							case XR_SPATIAL_PLANE_SEMANTIC_LABEL_UNCATEGORIZED_EXT: {
+								plane_tracker->set_plane_label("Uncategorized plane");
+							} break;
+							case XR_SPATIAL_PLANE_SEMANTIC_LABEL_FLOOR_EXT: {
+								plane_tracker->set_plane_label("Floor plane");
+							} break;
+							case XR_SPATIAL_PLANE_SEMANTIC_LABEL_WALL_EXT: {
+								plane_tracker->set_plane_label("Wall plane");
+							} break;
+							case XR_SPATIAL_PLANE_SEMANTIC_LABEL_CEILING_EXT: {
+								plane_tracker->set_plane_label("Ceiling plane");
+							} break;
+							case XR_SPATIAL_PLANE_SEMANTIC_LABEL_TABLE_EXT: {
+								plane_tracker->set_plane_label("Table plane");
+							} break;
+							default: {
+								plane_tracker->set_plane_label("Unknown plane");
+							} break;
+						}
+					}
+				}
+
+				if (add_to_xr_server) {
+					// Register with XR server
+					xr_server->add_tracker(plane_tracker);
+				}
+			}
+		}
+
+		// Remove any planes that are no longer there...
+		for (const XrSpatialEntityIdEXT &entity_id : current_planes) {
+			if (plane_trackers.has(entity_id)) {
+				Ref<OpenXRPlaneTracker> plane_tracker = plane_trackers[entity_id];
+
+				// Just in case there are still references out there to this marker,
+				// reset some stuff.
+				plane_tracker->invalidate_pose(SNAME("default"));
+				plane_tracker->set_spatial_tracking_state(XR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED_EXT);
+
+				// Remove it from our XRServer
+				xr_server->remove_tracker(plane_tracker);
+
+				// Remove it from our trackers
+				plane_trackers.erase(entity_id);
+			}
+		}
+	}
+
+	// Now that we're done, clean up our snapshot!
+	se_extension->free_spatial_snapshot(p_snapshot);
+
+	// And if this was our discovery snapshot, lets reset it
+	if (discovery_query_result.is_valid() && discovery_query_result->get_result_value() == p_snapshot) {
+		discovery_query_result.unref();
+	}
+}

+ 254 - 0
modules/openxr/extensions/spatial_entities/openxr_spatial_plane_tracking.h

@@ -0,0 +1,254 @@
+/**************************************************************************/
+/*  openxr_spatial_plane_tracking.h                                       */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "openxr_spatial_entities.h"
+#include "openxr_spatial_entity_extension.h"
+#include "scene/resources/3d/shape_3d.h"
+
+// Plane tracking capability configuration
+class OpenXRSpatialCapabilityConfigurationPlaneTracking : public OpenXRSpatialCapabilityConfigurationBaseHeader {
+	GDCLASS(OpenXRSpatialCapabilityConfigurationPlaneTracking, OpenXRSpatialCapabilityConfigurationBaseHeader);
+
+public:
+	virtual bool has_valid_configuration() const override;
+	virtual XrSpatialCapabilityConfigurationBaseHeaderEXT *get_configuration() override;
+
+	bool get_supports_mesh_2d();
+	bool get_supports_polygons();
+	bool get_supports_labels();
+
+	Vector<XrSpatialComponentTypeEXT> get_enabled_components() const { return plane_enabled_components; }
+
+protected:
+	static void _bind_methods();
+
+private:
+	int supports_mesh_2d = -1;
+	int supports_polygons = -1;
+	int supports_labels = -1;
+
+	Vector<XrSpatialComponentTypeEXT> plane_enabled_components;
+	XrSpatialCapabilityConfigurationPlaneTrackingEXT plane_config = { XR_TYPE_SPATIAL_CAPABILITY_CONFIGURATION_PLANE_TRACKING_EXT, nullptr, XR_SPATIAL_CAPABILITY_PLANE_TRACKING_EXT, 0, nullptr };
+
+	PackedInt64Array _get_enabled_components() const;
+};
+
+// Plane alignment component data
+class OpenXRSpatialComponentPlaneAlignmentList : public OpenXRSpatialComponentData {
+	GDCLASS(OpenXRSpatialComponentPlaneAlignmentList, OpenXRSpatialComponentData);
+
+public:
+	enum PlaneAlignment {
+		PLANE_ALIGNMENT_HORIZONTAL_UPWARD = XR_SPATIAL_PLANE_ALIGNMENT_HORIZONTAL_UPWARD_EXT,
+		PLANE_ALIGNMENT_HORIZONTAL_DOWNWARD = XR_SPATIAL_PLANE_ALIGNMENT_HORIZONTAL_DOWNWARD_EXT,
+		PLANE_ALIGNMENT_VERTICAL = XR_SPATIAL_PLANE_ALIGNMENT_VERTICAL_EXT,
+		PLANE_ALIGNMENT_ARBITRARY = XR_SPATIAL_PLANE_ALIGNMENT_ARBITRARY_EXT,
+	};
+
+	virtual void set_capacity(uint32_t p_capacity) override;
+	virtual XrSpatialComponentTypeEXT get_component_type() const override;
+	virtual void *get_structure_data(void *p_next) override;
+
+	XrSpatialPlaneAlignmentEXT get_plane_alignment(int64_t p_index) const;
+
+protected:
+	static void _bind_methods();
+
+private:
+	Vector<XrSpatialPlaneAlignmentEXT> plane_alignment_data;
+
+	XrSpatialComponentPlaneAlignmentListEXT plane_alignment_list = { XR_TYPE_SPATIAL_COMPONENT_PLANE_ALIGNMENT_LIST_EXT, nullptr, 0, nullptr };
+
+	PlaneAlignment _get_plane_alignment(int64_t p_index) const;
+};
+
+VARIANT_ENUM_CAST(OpenXRSpatialComponentPlaneAlignmentList::PlaneAlignment);
+
+class OpenXRSpatialComponentPolygon2DList : public OpenXRSpatialComponentData {
+	GDCLASS(OpenXRSpatialComponentPolygon2DList, OpenXRSpatialComponentData);
+
+protected:
+	static void _bind_methods();
+
+public:
+	virtual void set_capacity(uint32_t p_capacity) override;
+	virtual XrSpatialComponentTypeEXT get_component_type() const override;
+	virtual void *get_structure_data(void *p_next) override;
+
+	Transform3D get_transform(int64_t p_index) const;
+	PackedVector2Array get_vertices(RID p_snapshot, int64_t p_index) const;
+
+private:
+	Vector<XrSpatialPolygon2DDataEXT> polygon2d_data;
+
+	XrSpatialComponentPolygon2DListEXT polygon2d_list = { XR_TYPE_SPATIAL_COMPONENT_POLYGON_2D_LIST_EXT, nullptr, 0, nullptr };
+};
+
+// Plane semantic label component data.
+class OpenXRSpatialComponentPlaneSemanticLabelList : public OpenXRSpatialComponentData {
+	GDCLASS(OpenXRSpatialComponentPlaneSemanticLabelList, OpenXRSpatialComponentData);
+
+public:
+	enum PlaneSemanticLabel {
+		PLANE_SEMANTIC_LABEL_UNCATEGORIZED = XR_SPATIAL_PLANE_SEMANTIC_LABEL_UNCATEGORIZED_EXT,
+		PLANE_SEMANTIC_LABEL_FLOOR = XR_SPATIAL_PLANE_SEMANTIC_LABEL_FLOOR_EXT,
+		PLANE_SEMANTIC_LABEL_WALL = XR_SPATIAL_PLANE_SEMANTIC_LABEL_WALL_EXT,
+		PLANE_SEMANTIC_LABEL_CEILING = XR_SPATIAL_PLANE_SEMANTIC_LABEL_CEILING_EXT,
+		PLANE_SEMANTIC_LABEL_TABLE = XR_SPATIAL_PLANE_SEMANTIC_LABEL_TABLE_EXT,
+	};
+
+	virtual void set_capacity(uint32_t p_capacity) override;
+	virtual XrSpatialComponentTypeEXT get_component_type() const override;
+	virtual void *get_structure_data(void *p_next) override;
+
+	XrSpatialPlaneSemanticLabelEXT get_plane_semantic_label(int64_t p_index) const;
+
+protected:
+	static void _bind_methods();
+
+private:
+	Vector<XrSpatialPlaneSemanticLabelEXT> plane_semantic_label_data;
+
+	XrSpatialComponentPlaneSemanticLabelListEXT plane_semantic_label_list = { XR_TYPE_SPATIAL_COMPONENT_PLANE_SEMANTIC_LABEL_LIST_EXT, nullptr, 0, nullptr };
+
+	PlaneSemanticLabel _get_plane_semantic_label(int64_t p_index) const;
+};
+
+VARIANT_ENUM_CAST(OpenXRSpatialComponentPlaneSemanticLabelList::PlaneSemanticLabel);
+
+// Plane tracker
+class OpenXRPlaneTracker : public OpenXRSpatialEntityTracker {
+	GDCLASS(OpenXRPlaneTracker, OpenXRSpatialEntityTracker);
+
+public:
+	void set_bounds_size(const Vector2 &p_bounds_size);
+	Vector2 get_bounds_size() const;
+
+	void set_plane_alignment(OpenXRSpatialComponentPlaneAlignmentList::PlaneAlignment p_plane_alignment);
+	OpenXRSpatialComponentPlaneAlignmentList::PlaneAlignment get_plane_alignment() const;
+
+	void set_plane_label(const String &p_plane_label);
+	String get_plane_label() const;
+
+	void set_mesh_data(const Transform3D &p_origin, const PackedVector2Array &p_vertices, const PackedInt32Array &p_indices = PackedInt32Array());
+	void clear_mesh_data();
+
+	Transform3D get_mesh_offset() const;
+	Ref<Mesh> get_mesh();
+	Ref<Shape3D> get_shape(real_t p_thickness = 0.01);
+
+protected:
+	static void _bind_methods();
+
+private:
+	Vector2 bounds_size;
+	OpenXRSpatialComponentPlaneAlignmentList::PlaneAlignment plane_alignment = OpenXRSpatialComponentPlaneAlignmentList::PLANE_ALIGNMENT_HORIZONTAL_UPWARD;
+	String plane_label;
+
+	// Mesh data (if we have this)
+	struct MeshData {
+		bool has_mesh_data = false;
+		Transform3D origin;
+		PackedVector2Array vertices;
+		PackedInt32Array indices;
+
+		Ref<Mesh> mesh;
+		Ref<Shape3D> shape3d;
+	} mesh;
+
+	struct Edge {
+		int32_t a;
+		int32_t b;
+		static _FORCE_INLINE_ uint32_t hash(const Edge &p_edge) {
+			uint32_t h = hash_murmur3_one_32(p_edge.a);
+			return hash_murmur3_one_32(p_edge.b, h);
+		}
+		bool operator==(const Edge &p_edge) const {
+			return (a == p_edge.a && b == p_edge.b);
+		}
+
+		Edge(int32_t p_a = 0, int32_t p_b = 0) {
+			a = p_a;
+			b = p_b;
+			if (a < b) {
+				SWAP(a, b);
+			}
+		}
+	};
+};
+
+// Plane tracking logic
+class OpenXRSpatialPlaneTrackingCapability : public OpenXRExtensionWrapper {
+	GDCLASS(OpenXRSpatialPlaneTrackingCapability, OpenXRExtensionWrapper);
+
+protected:
+	static void _bind_methods();
+
+public:
+	static OpenXRSpatialPlaneTrackingCapability *get_singleton();
+
+	OpenXRSpatialPlaneTrackingCapability();
+	virtual ~OpenXRSpatialPlaneTrackingCapability() override;
+
+	virtual HashMap<String, bool *> get_requested_extensions() override;
+
+	virtual void on_session_created(const XrSession p_session) override;
+	virtual void on_session_destroyed() override;
+
+	virtual void on_process() override;
+
+	bool is_supported();
+
+private:
+	static OpenXRSpatialPlaneTrackingCapability *singleton;
+	bool spatial_plane_tracking_ext = false;
+	bool spatial_plane_tracking_supported = false;
+
+	RID spatial_context;
+	bool need_discovery = false;
+	int discovery_cooldown = 0;
+	Ref<OpenXRFutureResult> discovery_query_result;
+
+	Ref<OpenXRSpatialCapabilityConfigurationPlaneTracking> plane_configuration;
+
+	// Discovery logic
+	Ref<OpenXRFutureResult> _create_spatial_context();
+	void _on_spatial_context_created(RID p_spatial_context);
+
+	void _on_spatial_discovery_recommended(RID p_spatial_context);
+
+	Ref<OpenXRFutureResult> _start_entity_discovery();
+	void _process_snapshot(RID p_snapshot);
+
+	// Trackers
+	HashMap<XrSpatialEntityIdEXT, Ref<OpenXRPlaneTracker>> plane_trackers;
+};

+ 25 - 6
modules/openxr/openxr_api.cpp

@@ -307,20 +307,23 @@ String OpenXRAPI::get_default_action_map_resource_name() {
 	return name;
 }
 
-String OpenXRAPI::get_error_string(XrResult result) const {
-	if (XR_SUCCEEDED(result)) {
+String OpenXRAPI::get_error_string(XrResult p_result) const {
+	if (XR_SUCCEEDED(p_result)) {
 		return String("Succeeded");
 	}
 
 	if (instance == XR_NULL_HANDLE) {
-		Array args = { Variant(result) };
+		Array args = { Variant(p_result) };
 		return String("Error code {0}").format(args);
 	}
 
 	char resultString[XR_MAX_RESULT_STRING_SIZE];
-	xrResultToString(instance, result, resultString);
-
-	return String(resultString);
+	XrResult result = xrResultToString(instance, p_result, resultString);
+	if (XR_FAILED(result)) {
+		XR_ENUM_SWITCH(XrResult, p_result);
+	} else {
+		return String(resultString);
+	}
 }
 
 String OpenXRAPI::get_swapchain_format_name(int64_t p_swapchain_format) const {
@@ -2833,6 +2836,22 @@ Transform3D OpenXRAPI::transform_from_pose(const XrPosef &p_pose) {
 	return Transform3D(basis, origin);
 }
 
+XrPosef OpenXRAPI::pose_from_transform(const Transform3D &p_transform) {
+	XrPosef pose;
+
+	Quaternion q(p_transform.basis);
+	pose.orientation.x = q.x;
+	pose.orientation.y = q.y;
+	pose.orientation.z = q.z;
+	pose.orientation.w = q.w;
+
+	pose.position.x = p_transform.origin.x;
+	pose.position.y = p_transform.origin.y;
+	pose.position.z = p_transform.origin.z;
+
+	return pose;
+}
+
 template <typename T>
 XRPose::TrackingConfidence _transform_from_location(const T &p_location, Transform3D &r_transform) {
 	XRPose::TrackingConfidence confidence = XRPose::XR_TRACKING_CONFIDENCE_NONE;

+ 1 - 0
modules/openxr/openxr_api.h

@@ -430,6 +430,7 @@ public:
 
 	// helper method to convert an XrPosef to a Transform3D
 	Transform3D transform_from_pose(const XrPosef &p_pose);
+	XrPosef pose_from_transform(const Transform3D &p_transform);
 
 	// helper method to get a valid Transform3D from an openxr space location
 	XRPose::TrackingConfidence transform_from_location(const XrSpaceLocation &p_location, Transform3D &r_transform);

+ 83 - 0
modules/openxr/openxr_structure.cpp

@@ -0,0 +1,83 @@
+/**************************************************************************/
+/*  openxr_structure.cpp                                                  */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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 "openxr_structure.h"
+
+void OpenXRStructureBase::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_structure_type"), &OpenXRStructureBase::_get_structure_type);
+
+	ClassDB::bind_method(D_METHOD("set_next", "entity"), &OpenXRStructureBase::set_next);
+	ClassDB::bind_method(D_METHOD("get_next"), &OpenXRStructureBase::get_next);
+
+	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "next", PROPERTY_HINT_RESOURCE_TYPE, "OpenXRStructureBase"), "set_next", "get_next");
+
+	GDVIRTUAL_BIND(_get_header, "next");
+}
+
+void OpenXRStructureBase::set_next(const Ref<OpenXRStructureBase> p_next) {
+	next = p_next;
+}
+
+Ref<OpenXRStructureBase> OpenXRStructureBase::get_next() const {
+	return next;
+}
+
+void *OpenXRStructureBase::get_header(void *p_next) {
+	void *n = p_next;
+	if (get_next().is_valid()) {
+		n = get_next()->get_header(p_next);
+	}
+
+	uint64_t pointer = 0;
+
+	if (GDVIRTUAL_CALL(_get_header, (uint64_t)n, pointer)) {
+		return reinterpret_cast<void *>(pointer);
+	}
+
+	return n;
+}
+
+XrStructureType OpenXRStructureBase::get_structure_type() {
+	// By default we call get_header to get the structure type so we have a guaranteed implementation.
+
+	// The first member of our header is always the structure type, so this works:
+	XrStructureType *header = (XrStructureType *)get_header();
+	if (header == nullptr) {
+		// Header can return nullptr for valid reasons, so we do not error here!
+		return XR_TYPE_UNKNOWN;
+	} else {
+		return *header;
+	}
+}
+
+// Return structure type as uint64_t to GDScript
+uint64_t OpenXRStructureBase::_get_structure_type() {
+	return (uint64_t)get_structure_type();
+}

+ 76 - 0
modules/openxr/openxr_structure.h

@@ -0,0 +1,76 @@
+/**************************************************************************/
+/*  openxr_structure.h                                                    */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "core/object/gdvirtual.gen.inc"
+#include "core/object/ref_counted.h"
+#include "openxr_util.h"
+#include "util.h"
+
+// Base class for XrStructureType based headers
+class OpenXRStructureBase : public RefCounted {
+	GDCLASS(OpenXRStructureBase, RefCounted);
+
+public:
+	/*
+	 * get_header should return a pointer to a proper XrStructureType structure.
+	 * The pointer should remain valid as long as this object is not destructed.
+	 *
+	 * This function should be implemented based on the following template:
+	 * void *get_header(void *p_next = nullptr) {
+	 *     my_xr_struct.type = XR_TYPE_XYZ;
+	 *     if (get_next().is_valid()) {
+	 *         my_xr_struct.next = get_next()->get_header(p_next);
+	 *     } else {
+	 *         my_xr_struct.next = p_next
+	 *     }
+	 *
+	 *     // add further setup of the struct here
+	 *
+	 *     return &my_xr_struct;
+	 * }
+	 */
+	virtual void *get_header(void *p_next = nullptr);
+	virtual XrStructureType get_structure_type();
+
+	void set_next(const Ref<OpenXRStructureBase> p_next);
+	Ref<OpenXRStructureBase> get_next() const;
+
+	GDVIRTUAL1R(uint64_t, _get_header, uint64_t);
+
+protected:
+	static void _bind_methods();
+
+private:
+	Ref<OpenXRStructureBase> next;
+
+	uint64_t _get_structure_type();
+};

+ 102 - 8
modules/openxr/openxr_util.cpp

@@ -34,13 +34,9 @@
 
 #include <openxr/openxr_reflection.h>
 
-#define XR_ENUM_CASE_STR(name, val) \
-	case name:                      \
-		return #name;
-#define XR_ENUM_SWITCH(enumType, var)                                                                                           \
-	switch (var) {                                                                                                              \
-		XR_LIST_ENUM_##enumType(XR_ENUM_CASE_STR) default : return "Unknown " #enumType ": " + String::num_int64(int64_t(var)); \
-	}
+String OpenXRUtil::get_result_string(XrResult p_result){
+	XR_ENUM_SWITCH(XrResult, p_result)
+}
 
 String OpenXRUtil::get_view_configuration_name(XrViewConfigurationType p_view_configuration){
 	XR_ENUM_SWITCH(XrViewConfigurationType, p_view_configuration)
@@ -78,7 +74,105 @@ String OpenXRUtil::make_xr_version_string(XrVersion p_version) {
 	return version;
 }
 
-// Based on the OpenXR xr_linear.h private header, so we can still link against
+String OpenXRUtil::get_handle_as_hex_string(void *p_handle) {
+	String hex;
+
+	if (p_handle == XR_NULL_HANDLE) {
+		return "null";
+	}
+
+	uint64_t handle = (uint64_t)p_handle;
+
+	while (handle != 0) {
+		uint8_t a = handle & 0x0F;
+		uint8_t b = (handle & 0xF0) >> 4;
+		handle = handle >> 8;
+
+		if (a < 10) {
+			hex = (a + '0') + hex;
+		} else {
+			hex = (a + 'a' - 10) + hex;
+		}
+
+		if (b < 10) {
+			hex = (b + '0') + hex;
+		} else {
+			hex = (b + 'a' - 10) + hex;
+		}
+	}
+
+	return "0x" + hex;
+}
+
+String OpenXRUtil::string_from_xruuid(const XrUuid &xr_uuid) {
+	String ret;
+	bool non_zero = false;
+
+	for (int i = 0; i < XR_UUID_SIZE; i++) {
+		non_zero |= xr_uuid.data[i] != 0;
+
+		char a = xr_uuid.data[i] & 0xF0 >> 4;
+		char b = xr_uuid.data[i] & 0x0F;
+
+		if (a < 10) {
+			ret += '0' + a;
+		} else {
+			ret += 'a' + a - 10;
+		}
+
+		if (b < 10) {
+			ret += '0' + b;
+		} else {
+			ret += 'a' + b - 10;
+		}
+	}
+
+	if (non_zero) {
+		return ret;
+	} else {
+		return "";
+	}
+}
+
+XrUuid OpenXRUtil::xruuid_from_string(const String &p_uuid) {
+	XrUuid new_uuid = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
+
+	int len = p_uuid.length();
+	if (len == 0) {
+		return new_uuid;
+	} else if (len != (2 * XR_UUID_SIZE)) {
+		WARN_PRINT("OpenXR: Unexpected UUID length: " + String::num_int64(len) + " != " + String::num_int64(2 * XR_UUID_SIZE));
+	}
+
+	int j = 0;
+	for (int i = 0; i < XR_UUID_SIZE; i++) {
+		uint8_t val = 0;
+
+		// 2 chars per byte.
+		for (int k = 0; k < 2; k++) {
+			if (j < len) {
+				val <<= 4;
+
+				char32_t c = p_uuid[j++];
+				if (c >= '0' && c <= '9') {
+					val += uint8_t(c - '0');
+				} else if (c >= 'a' && c <= 'f') {
+					val += uint8_t(10 + c - 'a');
+				} else if (c >= 'A' && c <= 'F') {
+					val += uint8_t(10 + c - 'A');
+				} else {
+					WARN_PRINT("OpenXR: Unexpected character in UUID: " + String::num_int64(c));
+				}
+			}
+		}
+
+		new_uuid.data[i] = val;
+	}
+
+	return new_uuid;
+}
+
+// Copied from OpenXR xr_linear.h private header, so we can still link against
 // system-provided packages without relying on our `thirdparty` code.
 
 // Copyright (c) 2017 The Khronos Group Inc.

+ 13 - 0
modules/openxr/openxr_util.h

@@ -33,9 +33,19 @@
 #include "core/string/ustring.h"
 
 #include <openxr/openxr.h>
+#include <openxr/openxr_reflection.h>
+
+#define XR_ENUM_CASE_STR(name, val) \
+	case name:                      \
+		return #name;
+#define XR_ENUM_SWITCH(enumType, var)                                                                                           \
+	switch (var) {                                                                                                              \
+		XR_LIST_ENUM_##enumType(XR_ENUM_CASE_STR) default : return "Unknown " #enumType ": " + String::num_int64(int64_t(var)); \
+	}
 
 class OpenXRUtil {
 public:
+	static String get_result_string(XrResult p_result);
 	static String get_view_configuration_name(XrViewConfigurationType p_view_configuration);
 	static String get_reference_space_name(XrReferenceSpaceType p_reference_space);
 	static String get_structure_type_name(XrStructureType p_structure_type);
@@ -43,6 +53,9 @@ public:
 	static String get_action_type_name(XrActionType p_action_type);
 	static String get_environment_blend_mode_name(XrEnvironmentBlendMode p_blend_mode);
 	static String make_xr_version_string(XrVersion p_version);
+	static String get_handle_as_hex_string(void *p_handle);
+	static String string_from_xruuid(const XrUuid &xr_uuid);
+	static XrUuid xruuid_from_string(const String &p_uuid);
 
 	// Copied from OpenXR xr_linear.h private header, so we can still link against
 	// system-provided packages without relying on our `thirdparty` code.

+ 55 - 0
modules/openxr/register_types.cpp

@@ -75,6 +75,11 @@
 #include "extensions/openxr_valve_analog_threshold_extension.h"
 #include "extensions/openxr_visibility_mask_extension.h"
 #include "extensions/openxr_wmr_controller_extension.h"
+#include "extensions/spatial_entities/openxr_spatial_entity_extension.h"
+
+#include "extensions/spatial_entities/openxr_spatial_anchor.h"
+#include "extensions/spatial_entities/openxr_spatial_marker_tracking.h"
+#include "extensions/spatial_entities/openxr_spatial_plane_tracking.h"
 
 #ifdef TOOLS_ENABLED
 #include "editor/openxr_editor_plugin.h"
@@ -168,6 +173,23 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) {
 			OpenXRAPI::register_extension_wrapper(render_model_extension);
 			Engine::get_singleton()->add_singleton(Engine::Singleton("OpenXRRenderModelExtension", render_model_extension));
 
+			// Register spatial entity extensions
+			OpenXRSpatialEntityExtension *spatial_entity_extension = memnew(OpenXRSpatialEntityExtension);
+			OpenXRAPI::register_extension_wrapper(spatial_entity_extension);
+			Engine::get_singleton()->add_singleton(Engine::Singleton("OpenXRSpatialEntityExtension", spatial_entity_extension));
+
+			OpenXRSpatialAnchorCapability *anchor_capability = memnew(OpenXRSpatialAnchorCapability);
+			OpenXRAPI::register_extension_wrapper(anchor_capability);
+			Engine::get_singleton()->add_singleton(Engine::Singleton("OpenXRSpatialAnchorCapability", anchor_capability));
+
+			OpenXRSpatialPlaneTrackingCapability *plane_tracking_capability = memnew(OpenXRSpatialPlaneTrackingCapability);
+			OpenXRAPI::register_extension_wrapper(plane_tracking_capability);
+			Engine::get_singleton()->add_singleton(Engine::Singleton("OpenXRSpatialPlaneTrackingCapability", plane_tracking_capability));
+
+			OpenXRSpatialMarkerTrackingCapability *marker_tracking_capability = memnew(OpenXRSpatialMarkerTrackingCapability);
+			OpenXRAPI::register_extension_wrapper(marker_tracking_capability);
+			Engine::get_singleton()->add_singleton(Engine::Singleton("OpenXRSpatialMarkerTrackingCapability", marker_tracking_capability));
+
 			// register gated extensions
 			if (int(GLOBAL_GET("xr/openxr/extensions/debug_utils")) > 0) {
 				OpenXRAPI::register_extension_wrapper(memnew(OpenXRDebugUtilsExtension));
@@ -246,6 +268,39 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) {
 		GDREGISTER_CLASS(OpenXRRenderModel);
 		GDREGISTER_CLASS(OpenXRRenderModelManager);
 
+		GDREGISTER_CLASS(OpenXRSpatialEntityExtension);
+		GDREGISTER_VIRTUAL_CLASS(OpenXRSpatialEntityTracker);
+		GDREGISTER_CLASS(OpenXRAnchorTracker);
+		GDREGISTER_CLASS(OpenXRPlaneTracker);
+		GDREGISTER_CLASS(OpenXRMarkerTracker);
+
+		GDREGISTER_VIRTUAL_CLASS(OpenXRStructureBase);
+
+		GDREGISTER_VIRTUAL_CLASS(OpenXRSpatialCapabilityConfigurationBaseHeader);
+		GDREGISTER_CLASS(OpenXRSpatialCapabilityConfigurationAnchor);
+		GDREGISTER_CLASS(OpenXRSpatialCapabilityConfigurationQrCode);
+		GDREGISTER_CLASS(OpenXRSpatialCapabilityConfigurationMicroQrCode);
+		GDREGISTER_CLASS(OpenXRSpatialCapabilityConfigurationAruco);
+		GDREGISTER_CLASS(OpenXRSpatialCapabilityConfigurationAprilTag);
+		GDREGISTER_CLASS(OpenXRSpatialContextPersistenceConfig);
+		GDREGISTER_CLASS(OpenXRSpatialCapabilityConfigurationPlaneTracking);
+		GDREGISTER_VIRTUAL_CLASS(OpenXRSpatialComponentData);
+		GDREGISTER_CLASS(OpenXRSpatialComponentBounded2DList);
+		GDREGISTER_CLASS(OpenXRSpatialComponentBounded3DList);
+		GDREGISTER_CLASS(OpenXRSpatialComponentParentList);
+		GDREGISTER_CLASS(OpenXRSpatialComponentMesh2DList);
+		GDREGISTER_CLASS(OpenXRSpatialComponentMesh3DList);
+		GDREGISTER_CLASS(OpenXRSpatialComponentPlaneAlignmentList);
+		GDREGISTER_CLASS(OpenXRSpatialComponentPolygon2DList);
+		GDREGISTER_CLASS(OpenXRSpatialComponentPlaneSemanticLabelList);
+		GDREGISTER_CLASS(OpenXRSpatialComponentMarkerList);
+		GDREGISTER_CLASS(OpenXRSpatialQueryResultData);
+		GDREGISTER_CLASS(OpenXRSpatialComponentAnchorList);
+		GDREGISTER_CLASS(OpenXRSpatialComponentPersistenceList);
+		GDREGISTER_CLASS(OpenXRSpatialAnchorCapability);
+		GDREGISTER_CLASS(OpenXRSpatialPlaneTrackingCapability);
+		GDREGISTER_CLASS(OpenXRSpatialMarkerTrackingCapability);
+
 		XRServer *xr_server = XRServer::get_singleton();
 		if (xr_server) {
 			openxr_interface.instantiate();