Quellcode durchsuchen

New OpenXR ActionSets and InteractionProfiles assets

Per this RFC:
https://github.com/o3de/sig-graphics-audio/blob/main/rfcs/OpenXRActionsAndSpaces/rfc_openxr_actions_and_spaces.md
This commit is the first of several more that will be commited in different batches for the sake
of making it easier for code reviewers.

This commit has no runtime implications, other that registering
the OpenXRInteractionProfilesAsset and the OpenXRActionSetsAsset,
 along with the asset builder named OpenXRAssetsBuilder, which is in charge
 of validating and building both asset types.

OpenXRInteractionProfilesAsset will have the `*.xrprofiles` file extension.
An example asset is provided as the following path:
`Gems/OpenXRVk/Assets/OpenXRVk/system.xrprofiles`

OpenXRActionSetsAsset will have the `*.rxactions` file extension.
An example asset is provided as the following path:
`Gems/OpenXRVk/Assets/OpenXRVk/default.xractions`
This asset has a job dependency on `Gems/OpenXRVk/Assets/OpenXRVk/system.xrprofiles`.

Both asset types are also editable with the `Asset Editor`.

The Assets mentioned above will be compiled and placed in the asset cache
but they are unused at the moment for the sake of an easier code review.

Signed-off-by: galibzon <[email protected]>
galibzon vor 1 Jahr
Ursprung
Commit
73ed946b05

+ 25 - 0
Gems/OpenXRVk/Assets/OpenXRVk/README.md

@@ -0,0 +1,25 @@
+# About system.xrprofiles and default.xractions
+
+Both of these assets are editable via the `Asset Editor` UI. The `Asset Editor` is a tool provided by the O3DE Editor.
+
+`system.xrprofiles` defines a list of standard OpenXR interaction profiles that have been tested with O3DE.
+For more information read the header file: `.../Gems/OpenXRVk/Code/Include/OpenXRVk/OpenXRVkInteractionProfilesAsset.h`.
+
+`default.xraction` depends on `system.xrprofiles` and it defines a set of actions that your application can use
+to read user input or drive haptic feedback signals. This is the default asset that the OpenXRVk Gem will load, but can be overriden with help of the following Registry Key:
+**"/O3DE/Atom/OpenXR/ActionSetsAsset"**. If this key is not defined, the application will default to: **"openxrvk/default.xractions"**.  
+  
+Here is an example of an application named `AdventureVR` that customizes the Action Sets asset:
+- ActionSet Asset Location: \<AdventureVR\>/Assets/AdventureVR/adventurevr.xractions
+- Registry File Location: \<AdventureVR\>/Registry/adventurevr.setreg, with the following content:  
+```json
+{
+    "O3DE": {
+        "Atom": {
+            "OpenXR": {
+                "ActionSetsAsset": "adventurevr/adventurevr.xractions"
+            }
+        }
+    }
+}
+```

+ 101 - 0
Gems/OpenXRVk/Assets/OpenXRVk/default.xractions

@@ -0,0 +1,101 @@
+<ObjectStream version="3">
+	<Class name="OpenXRActionSetsAsset" version="1" type="{C2DEE370-6151-4701-AEA5-AEA3CA247CFF}">
+		<Class name="AssetData" field="BaseClass1" version="1" type="{AF3F7D32-1536-422A-89F3-A11E1F5B5A9C}"/>
+		<Class name="Asset&lt;OpenXRInteractionProfilesAsset&gt;" field="InteractionProfilesAsset" value="id={8DAACF0D-7509-5EF6-83E8-935550C77941}:0,type={02555DCD-E363-42FB-935C-4E67CC3A1699},hint={openxrvk/system.xrprofiles},loadBehavior=1" version="3" type="{5BB03E72-3A4C-56A3-82FE-DB0E915DC985}"/>
+		<Class name="AZStd::vector&lt;OpenXRActionSetDescriptor, allocator&gt;" field="ActionSetDescriptors" type="{77143880-BF52-5468-B42A-0485DA0C32F7}">
+			<Class name="OpenXRActionSetDescriptor" field="element" version="1" type="{3A08BC1F-656F-441F-89C3-829F95B9B329}">
+				<Class name="AZStd::string" field="Name" value="main_action_set" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+				<Class name="AZStd::string" field="LocalizedName" value="Main Action Set" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+				<Class name="unsigned int" field="Priority" value="0" type="{43DA906B-7DEF-4CA8-9790-854106D3F983}"/>
+				<Class name="AZStd::string" field="Comment" value="A simple action set." type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+				<Class name="AZStd::vector&lt;OpenXRActionDescriptor, allocator&gt;" field="ActionDescriptors" type="{24F3EBED-4499-5592-9413-F5C010362529}">
+					<Class name="OpenXRActionDescriptor" field="element" version="1" type="{90BBF6F6-C7D6-4F64-B784-CE03F86DC36B}">
+						<Class name="AZStd::string" field="Name" value="move_frontways" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="LocalizedName" value="Move Frontways" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Comment" value="" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::vector&lt;OpenXRActionPathDescriptor, allocator&gt;" field="ActionPathDescriptors" type="{6A1AA49C-7A58-5B23-8B25-30B162E65135}">
+							<Class name="OpenXRActionPathDescriptor" field="element" version="1" type="{F25D6382-C9E0-414B-A542-1758F5477D03}">
+								<Class name="AZStd::string" field="InteractionProfile" value="Oculus Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="UserPath" value="(L)" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ComponentPath" value="Thumbstick Y" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+						</Class>
+					</Class>
+					<Class name="OpenXRActionDescriptor" field="element" version="1" type="{90BBF6F6-C7D6-4F64-B784-CE03F86DC36B}">
+						<Class name="AZStd::string" field="Name" value="move_sideways" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="LocalizedName" value="Move Sideways" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Comment" value="" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::vector&lt;OpenXRActionPathDescriptor, allocator&gt;" field="ActionPathDescriptors" type="{6A1AA49C-7A58-5B23-8B25-30B162E65135}">
+							<Class name="OpenXRActionPathDescriptor" field="element" version="1" type="{F25D6382-C9E0-414B-A542-1758F5477D03}">
+								<Class name="AZStd::string" field="InteractionProfile" value="Oculus Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="UserPath" value="(L)" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ComponentPath" value="Thumbstick X" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+						</Class>
+					</Class>
+					<Class name="OpenXRActionDescriptor" field="element" version="1" type="{90BBF6F6-C7D6-4F64-B784-CE03F86DC36B}">
+						<Class name="AZStd::string" field="Name" value="shift_yaw_rotate" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="LocalizedName" value="Shift Yaw Rotate" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Comment" value="" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::vector&lt;OpenXRActionPathDescriptor, allocator&gt;" field="ActionPathDescriptors" type="{6A1AA49C-7A58-5B23-8B25-30B162E65135}">
+							<Class name="OpenXRActionPathDescriptor" field="element" version="1" type="{F25D6382-C9E0-414B-A542-1758F5477D03}">
+								<Class name="AZStd::string" field="InteractionProfile" value="Oculus Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="UserPath" value="(R)" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ComponentPath" value="Thumbstick X" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+						</Class>
+					</Class>
+					<Class name="OpenXRActionDescriptor" field="element" version="1" type="{90BBF6F6-C7D6-4F64-B784-CE03F86DC36B}">
+						<Class name="AZStd::string" field="Name" value="move_up" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="LocalizedName" value="Move Up" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Comment" value="" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::vector&lt;OpenXRActionPathDescriptor, allocator&gt;" field="ActionPathDescriptors" type="{6A1AA49C-7A58-5B23-8B25-30B162E65135}">
+							<Class name="OpenXRActionPathDescriptor" field="element" version="1" type="{F25D6382-C9E0-414B-A542-1758F5477D03}">
+								<Class name="AZStd::string" field="InteractionProfile" value="Oculus Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="UserPath" value="(L)" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ComponentPath" value="Y Click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+						</Class>
+					</Class>
+					<Class name="OpenXRActionDescriptor" field="element" version="1" type="{90BBF6F6-C7D6-4F64-B784-CE03F86DC36B}">
+						<Class name="AZStd::string" field="Name" value="move_down" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="LocalizedName" value="Move Down" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Comment" value="" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::vector&lt;OpenXRActionPathDescriptor, allocator&gt;" field="ActionPathDescriptors" type="{6A1AA49C-7A58-5B23-8B25-30B162E65135}">
+							<Class name="OpenXRActionPathDescriptor" field="element" version="1" type="{F25D6382-C9E0-414B-A542-1758F5477D03}">
+								<Class name="AZStd::string" field="InteractionProfile" value="Oculus Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="UserPath" value="(L)" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ComponentPath" value="X Click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+						</Class>
+					</Class>
+					<Class name="OpenXRActionDescriptor" field="element" version="1" type="{90BBF6F6-C7D6-4F64-B784-CE03F86DC36B}">
+						<Class name="AZStd::string" field="Name" value="b_button_click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="LocalizedName" value="Right Hand B Click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Comment" value="" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::vector&lt;OpenXRActionPathDescriptor, allocator&gt;" field="ActionPathDescriptors" type="{6A1AA49C-7A58-5B23-8B25-30B162E65135}">
+							<Class name="OpenXRActionPathDescriptor" field="element" version="1" type="{F25D6382-C9E0-414B-A542-1758F5477D03}">
+								<Class name="AZStd::string" field="InteractionProfile" value="Oculus Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="UserPath" value="(R)" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ComponentPath" value="B Click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+						</Class>
+					</Class>
+					<Class name="OpenXRActionDescriptor" field="element" version="1" type="{90BBF6F6-C7D6-4F64-B784-CE03F86DC36B}">
+						<Class name="AZStd::string" field="Name" value="a_button_click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="LocalizedName" value="Right Hand A Click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Comment" value="" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::vector&lt;OpenXRActionPathDescriptor, allocator&gt;" field="ActionPathDescriptors" type="{6A1AA49C-7A58-5B23-8B25-30B162E65135}">
+							<Class name="OpenXRActionPathDescriptor" field="element" version="1" type="{F25D6382-C9E0-414B-A542-1758F5477D03}">
+								<Class name="AZStd::string" field="InteractionProfile" value="Oculus Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="UserPath" value="(R)" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ComponentPath" value="A Click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+						</Class>
+					</Class>
+				</Class>
+			</Class>
+		</Class>
+	</Class>
+</ObjectStream>
+

+ 171 - 0
Gems/OpenXRVk/Assets/OpenXRVk/system.xrprofiles

@@ -0,0 +1,171 @@
+<ObjectStream version="3">
+	<Class name="OpenXRInteractionProfilesAsset" version="1" type="{02555DCD-E363-42FB-935C-4E67CC3A1699}">
+		<Class name="AssetData" field="BaseClass1" version="1" type="{AF3F7D32-1536-422A-89F3-A11E1F5B5A9C}"/>
+		<Class name="AZStd::vector&lt;OpenXRInteractionProfileDescriptor, allocator&gt;" field="InteractionProfiles" type="{3C0349C5-3973-557D-81A6-D8DED0B8F61B}">
+			<Class name="OpenXRInteractionProfileDescriptor" field="element" version="1" type="{BC73B4BC-4F15-4B1E-AEA9-B133FBB5AD16}">
+				<Class name="AZStd::string" field="UniqueName" value="Khronos Simple" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+				<Class name="AZStd::string" field="Path" value="/interaction_profiles/khr/simple_controller" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+				<Class name="AZStd::vector&lt;OpenXRInteractionUserPathDescriptor, allocator&gt;" field="UserPathDescriptors" type="{9CEF8D8D-9393-529F-A3C6-C67422B80D96}">
+					<Class name="OpenXRInteractionUserPathDescriptor" field="element" version="1" type="{F3913A15-41FC-4EC9-A381-296C0AB6D6C6}">
+						<Class name="AZStd::string" field="Name" value="(L)" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/user/hand/left" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::vector&lt;OpenXRInteractionComponentPathDescriptor, allocator&gt;" field="ComponentPaths" type="{3B1B11C6-132C-509E-A79E-B04AB829CFA7}"/>
+					</Class>
+					<Class name="OpenXRInteractionUserPathDescriptor" field="element" version="1" type="{F3913A15-41FC-4EC9-A381-296C0AB6D6C6}">
+						<Class name="AZStd::string" field="Name" value="(R)" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/user/hand/right" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::vector&lt;OpenXRInteractionComponentPathDescriptor, allocator&gt;" field="ComponentPaths" type="{3B1B11C6-132C-509E-A79E-B04AB829CFA7}"/>
+					</Class>
+				</Class>
+				<Class name="AZStd::vector&lt;OpenXRInteractionComponentPathDescriptor, allocator&gt;" field="CommonComponentPathDescriptors" type="{3B1B11C6-132C-509E-A79E-B04AB829CFA7}">
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Select Click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/input/select/click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Menu Click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/input/menu/click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Grip Pose" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/input/grip/pose" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Vibration" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Aim Pose" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/input/aim/pose" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Pose" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Output Vibration" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/output/haptic" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Vibration" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+				</Class>
+			</Class>
+			<Class name="OpenXRInteractionProfileDescriptor" field="element" version="1" type="{BC73B4BC-4F15-4B1E-AEA9-B133FBB5AD16}">
+				<Class name="AZStd::string" field="UniqueName" value="Oculus Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+				<Class name="AZStd::string" field="Path" value="/interaction_profiles/oculus/touch_controller" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+				<Class name="AZStd::vector&lt;OpenXRInteractionUserPathDescriptor, allocator&gt;" field="UserPathDescriptors" type="{9CEF8D8D-9393-529F-A3C6-C67422B80D96}">
+					<Class name="OpenXRInteractionUserPathDescriptor" field="element" version="1" type="{F3913A15-41FC-4EC9-A381-296C0AB6D6C6}">
+						<Class name="AZStd::string" field="Name" value="(L)" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/user/hand/left" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::vector&lt;OpenXRInteractionComponentPathDescriptor, allocator&gt;" field="ComponentPaths" type="{3B1B11C6-132C-509E-A79E-B04AB829CFA7}">
+							<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+								<Class name="AZStd::string" field="Name" value="X Click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="Path" value="/input/x/click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+							<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+								<Class name="AZStd::string" field="Name" value="X Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="Path" value="/input/x/touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+							<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+								<Class name="AZStd::string" field="Name" value="Y Click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="Path" value="/input/y/click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+							<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+								<Class name="AZStd::string" field="Name" value="Y Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="Path" value="/input/y/touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+							<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+								<Class name="AZStd::string" field="Name" value="Menu Click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="Path" value="/input/menu/click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+						</Class>
+					</Class>
+					<Class name="OpenXRInteractionUserPathDescriptor" field="element" version="1" type="{F3913A15-41FC-4EC9-A381-296C0AB6D6C6}">
+						<Class name="AZStd::string" field="Name" value="(R)" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/user/hand/right" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::vector&lt;OpenXRInteractionComponentPathDescriptor, allocator&gt;" field="ComponentPaths" type="{3B1B11C6-132C-509E-A79E-B04AB829CFA7}">
+							<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+								<Class name="AZStd::string" field="Name" value="A Click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="Path" value="/input/a/click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+							<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+								<Class name="AZStd::string" field="Name" value="A Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="Path" value="/input/a/touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+							<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+								<Class name="AZStd::string" field="Name" value="B Click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="Path" value="/input/b/click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+							<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+								<Class name="AZStd::string" field="Name" value="B Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="Path" value="/input/b/touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+								<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+							</Class>
+						</Class>
+					</Class>
+				</Class>
+				<Class name="AZStd::vector&lt;OpenXRInteractionComponentPathDescriptor, allocator&gt;" field="CommonComponentPathDescriptors" type="{3B1B11C6-132C-509E-A79E-B04AB829CFA7}">
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Squeeze" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/input/squeeze/value" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Float" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Trigger Value" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/input/trigger/value" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Float" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Trigger Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/input/trigger/touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Thumbstick X" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/input/thumbstick/x" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Float" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Thumbstick Y" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/input/thumbstick/y" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Float" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Thumbstick Click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/input/thumbstick/click" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Thumbstick Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/input/thumbstick/touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Thumbrest Touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/input/thumbrest/touch" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Boolean" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Grip Pose" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/input/grip/pose" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Pose" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Aim Pose" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/input/aim/pose" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Pose" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+					<Class name="OpenXRInteractionComponentPathDescriptor" field="element" version="1" type="{E2038854-929D-484F-A34E-1C7390EE2CCB}">
+						<Class name="AZStd::string" field="Name" value="Vibrate" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="Path" value="/output/haptic" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+						<Class name="AZStd::string" field="ActionType" value="Vibration" type="{03AAAB3F-5C47-5A66-9EBC-D5FA4DB353C9}"/>
+					</Class>
+				</Class>
+			</Class>
+		</Class>
+	</Class>
+</ObjectStream>
+

+ 65 - 7
Gems/OpenXRVk/Code/CMakeLists.txt

@@ -6,6 +6,10 @@
 #
 #
 
+# Queries the "gem_name" "version" values from the gem.json file for this gem
+# They are set in the ${gem_name} and ${gem_version} variables
+o3de_gem_setup("${Name}")
+
 o3de_pal_dir(pal_include_dir ${CMAKE_CURRENT_LIST_DIR}/Include/OpenXRVk/Platform/${PAL_PLATFORM_NAME} "${gem_restricted_path}" "${gem_path}" "${gem_parent_relative_path}")
 o3de_pal_dir(pal_source_dir ${CMAKE_CURRENT_LIST_DIR}/Source/Platform/${PAL_PLATFORM_NAME} "${gem_restricted_path}" "${gem_path}" "${gem_parent_relative_path}")
 
@@ -19,7 +23,7 @@ if(NOT PAL_TRAIT_OPENXRVK_SUPPORTED)
 
     # Create stub modules. Once we support gem loading configuration, we can remove this stubbed targets
     ly_add_target(
-        NAME OpenXRVk ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE}
+        NAME ${gem_name} ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE}
         NAMESPACE Gem
         FILES_CMAKE
             openxrvk_stub_module.cmake
@@ -36,7 +40,7 @@ if(NOT PAL_TRAIT_OPENXRVK_SUPPORTED)
 endif()
 
 ly_add_target(
-    NAME OpenXRVk.Static STATIC
+    NAME ${gem_name}.Static STATIC
     NAMESPACE Gem
     FILES_CMAKE
         openxrvk_private_common_files.cmake
@@ -63,7 +67,7 @@ ly_add_target(
 )
 
 ly_add_target(
-    NAME OpenXRVk ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE}
+    NAME ${gem_name} ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE}
     NAMESPACE Gem
     FILES_CMAKE
         openxrvk_private_common_shared_files.cmake
@@ -76,7 +80,7 @@ ly_add_target(
             ${pal_include_dir}
     BUILD_DEPENDENCIES
         PRIVATE
-            Gem::OpenXRVk.Static
+            Gem::${gem_name}.Static
 )
 
 # use the OpenXRVk module in all aliases:
@@ -84,8 +88,62 @@ ly_create_alias(NAME OpenXRVk.Clients NAMESPACE Gem TARGETS Gem::OpenXRVk)
 ly_create_alias(NAME OpenXRVk.Unified NAMESPACE Gem TARGETS Gem::OpenXRVk)
 
 if(PAL_TRAIT_BUILD_HOST_TOOLS)
-    ly_create_alias(NAME OpenXRVk.Tools NAMESPACE Gem TARGETS Gem::OpenXRVk)
-    ly_create_alias(NAME OpenXRVk.Builders NAMESPACE Gem TARGETS Gem::OpenXRVk)
+
+    #
+    ly_add_target(
+        NAME ${gem_name}.Builders.Static STATIC
+        NAMESPACE Gem
+        FILES_CMAKE
+            openxrvk_private_builder_files.cmake
+        INCLUDE_DIRECTORIES
+            PRIVATE
+                Source
+            PUBLIC
+                Include
+        BUILD_DEPENDENCIES
+            PRIVATE
+                AZ::AzCore
+                AZ::AzFramework
+                AZ::AssetBuilderSDK
+                ${openxr_dependency}
+                Gem::${gem_name}.Static
+    )
+
+    # It is very important that the target name ends with "*.Buiders"
+    # because it maakes the module loadable by AssetBuilder.exe.
+    ly_add_target(
+        NAME ${gem_name}.Builders GEM_MODULE
+        NAMESPACE Gem
+        FILES_CMAKE
+            openxrvk_shared_builder_files.cmake
+        INCLUDE_DIRECTORIES
+            PRIVATE
+                Source
+            PUBLIC
+                Include
+        BUILD_DEPENDENCIES
+            PRIVATE
+                AZ::AzCore
+                AZ::AzFramework
+                AZ::AssetBuilderSDK
+                Gem::${gem_name}.Builders.Static
+    )
+
+     # Inject the gem name into the Module source file
+     ly_add_source_properties(
+        SOURCES
+            Source/Builders/OpenXRVkBuilderModule.cpp
+        PROPERTY COMPILE_DEFINITIONS
+            VALUES
+                O3DE_GEM_NAME=${gem_name}
+                O3DE_GEM_VERSION=${gem_version}
+    )
+
+    # Create an alias for the tool version.
+    # This is necessary so the Editor loads the Module. And the trick
+    # is to make sure the alias name ends in ".Tools"
+    ly_create_alias(NAME ${gem_name}.Tools NAMESPACE Gem TARGETS Gem::${gem_name})
+
 endif()
 
 ################################################################################
@@ -116,4 +174,4 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
         NAME Gem::OpenXRVk.Tests
     )
 
-endif()
+endif()

+ 165 - 0
Gems/OpenXRVk/Code/Include/OpenXRVk/OpenXRVkActionSetsAsset.h

@@ -0,0 +1,165 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/Asset/AssetCommon.h>
+
+#include <OpenXRVk/OpenXRVkInteractionProfilesAsset.h>
+
+namespace OpenXRVk
+{
+    //! An Action Path Descriptor is nothing more than a tuple of three
+    //! strings that identify a unique Input or Haptic control for a particular
+    //! vendor equipment. The interesting point is that these strings MUST be limited
+    //! to the unique names provided by an OpenXRInteractionProfileAsset.
+    class OpenXRActionPathDescriptor final
+    {
+    public:
+        AZ_RTTI(OpenXRActionPathDescriptor, "{F25D6382-C9E0-414B-A542-1758F5477D03}");
+        virtual ~OpenXRActionPathDescriptor() = default;
+
+        static void Reflect(AZ::ReflectContext* reflection);
+
+        AZStd::string GetEditorText() const;
+
+        //! Should match an OpenXRInteractionProfileDescriptor::m_name
+        AZStd::string m_interactionProfileName;
+        
+        //! Should match an OpenXRInteractionUserPathDescriptor::m_name
+        AZStd::string m_userPathName;
+
+        //! Should match an OpenXRInteractionComponentPathDescriptor::m_name
+        AZStd::string m_componentPathName;
+
+    private:
+        AZ::Crc32 OnInteractionProfileSelected();
+        AZStd::vector<AZStd::string> GetInteractionProfiles() const;
+
+        AZ::Crc32 OnUserPathSelected();
+        AZStd::vector<AZStd::string> GetUserPaths() const;
+
+        AZ::Crc32 OnComponentPathSelected();
+        AZStd::vector<AZStd::string> GetComponentPaths() const;
+    };
+
+    //! Describes a custom Action I/O that will be queried/driven
+    //! by the application gameplay.
+    class OpenXRActionDescriptor final
+    {
+    public:
+        AZ_RTTI(OpenXRActionDescriptor, "{90BBF6F6-C7D6-4F64-B784-CE03F86DC36B}");
+        virtual ~OpenXRActionDescriptor() = default;
+
+        static void Reflect(AZ::ReflectContext* reflection);
+
+        AZStd::string GetEditorText() const;
+
+        //! This name must be unique across all Actions listed in an Action Set.
+        //! The content of this string is limited to the characters listed here:
+        //! https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#well-formed-path-strings 
+        AZStd::string m_name; // Regular char*
+
+        //! User friendly name.
+        AZStd::string m_localizedName; // UTF-8 string.
+
+        //! Free form comment about this Action.
+        AZStd::string m_comment;
+
+        //! List of I/O action paths that will be bound to this action.
+        //! The first action path in this list, determines what type of action paths
+        //! can be added to the list. For example:
+        //! If the first action path happens to be a boolean, then subsequent action paths
+        //! can only be added if they can map to a boolean.
+        //! Another important case is if the this is a haptic feedback action (Output), then
+        //! subsequent action paths can only be of type haptic feedback actions.
+        AZStd::vector<OpenXRActionPathDescriptor> m_actionPathDescriptors;
+    };
+
+    //! Describes a custom Action Set. All applications
+    //! will have custom Action Sets because that's how developers define
+    //! the gameplay I/O.
+    class OpenXRActionSetDescriptor final
+    {
+    public:
+        AZ_RTTI(OpenXRActionSetDescriptor, "{3A08BC1F-656F-441F-89C3-829F95B9B329}");
+        virtual ~OpenXRActionSetDescriptor() = default;
+
+        static void Reflect(AZ::ReflectContext* reflection);
+
+        AZStd::string GetEditorText() const;
+
+        //! This name must be unique across all Action Sets listed in an Action Sets Asset.
+        //! The content of this string is limited to the characters listed here:
+        //! https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#well-formed-path-strings 
+        AZStd::string m_name;
+
+        //! User friendly name.
+        AZStd::string m_localizedName; // UTF-8 string.
+        
+        //! Higher values mean higher priority.
+        //! The priority is used by the OpenXR runtime in case several action sets
+        //! use identical action paths and the highest priority will win the event.
+        uint32_t m_priority = 0;
+        
+        //! Free form comment about this Action Set.
+        AZStd::string m_comment;
+
+        //! List of all actions under this Action Set.
+        AZStd::vector<OpenXRActionDescriptor> m_actionDescriptors;
+    };
+
+    //! This asset defines a list of  OpenXR Action Sets that an application supports
+    //! regarding inputs and haptics.
+    class OpenXRActionSetsAsset final
+        : public AZ::Data::AssetData
+    {
+    public:
+        AZ_CLASS_ALLOCATOR(OpenXRActionSetsAsset, AZ::SystemAllocator);
+        AZ_RTTI(OpenXRActionSetsAsset, "{C2DEE370-6151-4701-AEA5-AEA3CA247CFF}", AZ::Data::AssetData);
+        static void Reflect(AZ::ReflectContext* context);
+
+        static constexpr char s_assetTypeName[] = "OpenXR Action Sets Asset";
+        static constexpr char s_assetExtension[] = "xractions";
+
+        //! By referencing a particular Interaction Profiles asset, the actions
+        //! exposed in this Action Sets asset will be limited to the vendor support
+        //! profiles listed in the Interaction Profiles asset.
+        AZ::Data::Asset<OpenXRInteractionProfilesAsset> m_interactionProfilesAsset;
+
+        //! List of all Action Sets the application will work with.
+        AZStd::vector<OpenXRActionSetDescriptor> m_actionSetDescriptors;
+
+    private:
+        AZ::Crc32 OnInteractionProfilesAssetChanged();
+    };
+
+    //! We need a custom asset handler because OpenXRActionSetsAsset contains a reference to another
+    //! asset of type OpenXRInteractionProfilesAsset and we need to set a static singleton of type
+    //! OpenXRInteractionProfilesAsset when the user is creating/editing an OpenXRActionSetsAsset with
+    //! the Asset Editor. 
+    class OpenXRActionSetsAssetHandler final
+        : public AzFramework::GenericAssetHandler<OpenXRActionSetsAsset>
+    {
+    public:
+        AZ_RTTI(OpenXRActionSetsAssetHandler, "{1C4A27E9-6768-4C59-9582-2A01A0DEC1D0}", AzFramework::GenericAssetHandler<OpenXRActionSetsAsset>);
+        AZ_CLASS_ALLOCATOR(OpenXRActionSetsAssetHandler, AZ::SystemAllocator);
+    
+        static constexpr char LogName[] = "OpenXRInteractionProfilesAssetHandler";
+    
+        OpenXRActionSetsAssetHandler();
+    
+        // Called by the asset manager to perform actual asset load.
+        AZ::Data::AssetHandler::LoadResult LoadAssetData(
+            const AZ::Data::Asset<AZ::Data::AssetData>& asset,
+            AZStd::shared_ptr<AZ::Data::AssetDataStream> stream,
+            const AZ::Data::AssetFilterCB& assetLoadFilterCB) override;
+
+        bool SaveAssetData(const AZ::Data::Asset<AZ::Data::AssetData>& asset, AZ::IO::GenericStream* stream) override;
+    };
+}// namespace OpenXRVk

+ 32 - 0
Gems/OpenXRVk/Code/Include/OpenXRVk/OpenXRVkAssetsValidator.h

@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/Asset/AssetCommon.h>
+
+#include <OpenXRVk/OpenXRVkInteractionProfilesAsset.h>
+#include <OpenXRVk/OpenXRVkActionSetsAsset.h>
+
+//! API that validates the content of InteractionProfiles assets and ActionSets assets.
+//! In principle, this API doesn't belong in the main OpenXRVk Runtime, instead, it should be
+//! private to the Asset Builders. BUT, at the moment we rely on the Asset Editor as a means to
+//! provide the application developer with an UI to create these kind of assets. We need to make sure
+//! the assets are valid before the user can save them from Asset Editor -> Save menu option to prevent
+//! asset build failure which would cause these assets to become uneditable in the Asset Editor.
+//! TODO: Develop a custom tool, either in C++ or Python to edit InteractionProfiles assets and ActionSets assets
+//! and move this API to the OpenXRVk.Builders.Static target.
+namespace OpenXRVkAssetsValidator
+{
+    AZ::Outcome<void, AZStd::string> ValidateInteractionProfilesAsset(
+        const OpenXRVk::OpenXRInteractionProfilesAsset& interactionProfilesAsset);
+
+    AZ::Outcome<void, AZStd::string> ValidateActionSetsAsset(const OpenXRVk::OpenXRActionSetsAsset& actionSetsAsset,
+        const OpenXRVk::OpenXRInteractionProfilesAsset& interactionProfilesAsset);
+
+}// namespace OpenXRVkAssetsValidator

+ 157 - 0
Gems/OpenXRVk/Code/Include/OpenXRVk/OpenXRVkInteractionProfilesAsset.h

@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <openxr/openxr.h>
+
+#include <AzCore/Asset/AssetCommon.h>
+#include <AzFramework/Asset/GenericAssetHandler.h>
+
+namespace OpenXRVk
+{
+    //! A Component Path Descriptor identifies Inputs or Haptics
+    //! available in a particular controller Like the 'X' or 'Y' Buttons
+    //! or the ability to vibrate (Haptic)
+    class OpenXRInteractionComponentPathDescriptor final
+    {
+    public:
+        AZ_RTTI(OpenXRInteractionComponentPathDescriptor, "{E2038854-929D-484F-A34E-1C7390EE2CCB}");
+        virtual ~OpenXRInteractionComponentPathDescriptor() = default;
+
+        static void Reflect(AZ::ReflectContext* reflection);
+
+        static constexpr AZStd::string_view s_TypeBoolStr = "Boolean";
+        static constexpr AZStd::string_view s_TypeFloatStr = "Float";
+        static constexpr AZStd::string_view s_TypeVector2Str = "Vector2";
+        static constexpr AZStd::string_view s_TypePoseStr = "Pose";
+        static constexpr AZStd::string_view s_TypeVibrationStr = "Vibration";
+
+        //! Helper method
+        static XrActionType GetXrActionType(AZStd::string_view actionTypeStr);
+        XrActionType GetXrActionType() const;
+
+        //! A user friendly name.
+        AZStd::string m_name;
+        //! For OpenXR a Component Path string would look like:
+        //! "/input/x/click", or "/input/trigger/value", etc
+        AZStd::string m_path;
+        //! Whether this is a boolean, float, vector2 or pose.
+        //! The user will be presented with a combo box to avoid
+        //! chances for error.
+        AZStd::string m_actionTypeStr;
+
+    private:
+        AZStd::string GetEditorText();
+
+    };
+
+    //! A User Path descriptor describes the XrPath (as a string) that will be
+    //! use to identify a Left or Right hand controller, or a Game pad controller. 
+    class OpenXRInteractionUserPathDescriptor final
+    {
+    public:
+        AZ_RTTI(OpenXRInteractionUserPathDescriptor, "{F3913A15-41FC-4EC9-A381-296C0AB6D6C6}");
+        virtual ~OpenXRInteractionUserPathDescriptor() = default;
+
+        static void Reflect(AZ::ReflectContext* reflection);
+        const OpenXRInteractionComponentPathDescriptor* GetComponentPathDescriptor(const AZStd::string& componentPathName) const;
+
+        //! A user friendly name.
+        AZStd::string m_name;
+        //! For OpenXR a User Path string would look like:
+        //! "/user/hand/left", or "/user/hand/right", etc
+        AZStd::string m_path;
+        //! Component Paths that are only supported under this User Path.
+        //! This list can be empty. In case it is empty, it means that all component Paths
+        //! are listed under the Interaction Profile Descriptor that owns this User Path Descriptor.
+        AZStd::vector<OpenXRInteractionComponentPathDescriptor> m_componentPathDescriptors;
+
+    private:
+        AZStd::string GetEditorText();
+
+    };
+
+    //! An Interaction Profile descriptor describes all the User Paths and Component Paths that
+    //! a particular Vendor Equipment supports.  
+    class OpenXRInteractionProfileDescriptor final
+    {
+    public:
+        AZ_RTTI(OpenXRInteractionProfileDescriptor, "{BC73B4BC-4F15-4B1E-AEA9-B133FBB5AD16}");
+        virtual ~OpenXRInteractionProfileDescriptor() = default;
+
+        static void Reflect(AZ::ReflectContext* reflection);
+
+        static constexpr char LogName[] = "OpenXRInteractionProfileDescriptor";
+
+        const OpenXRInteractionUserPathDescriptor* GetUserPathDescriptor(const AZStd::string& userPathName) const;
+        const OpenXRInteractionComponentPathDescriptor* GetCommonComponentPathDescriptor(const AZStd::string& componentPathName) const;
+        const OpenXRInteractionComponentPathDescriptor* GetComponentPathDescriptor(const OpenXRInteractionUserPathDescriptor& userPathDescriptor, const AZStd::string& componentPathName) const;
+        AZStd::string GetComponentAbsolutePath(const OpenXRInteractionUserPathDescriptor& userPathDescriptor, const AZStd::string& componentPathName) const;
+
+        //! Unique name across all OpenXRInteractionProfileDescriptor.
+        //! It serves also as user friendly display name, and because
+        //! it is unique it can be used in a dictionary.
+        AZStd::string m_name;
+        //! A string convertible to XrPath like:
+        //! "/interaction_profiles/khr/simple_controller", or
+        //! "/interaction_profiles/oculus/touch_controller"
+        AZStd::string m_path;
+
+        //! All the User Paths that this equipment supports.
+        AZStd::vector<OpenXRInteractionUserPathDescriptor> m_userPathDescriptors;
+
+        //! Common Component Paths that are supported by all User Paths listed in @m_userPathDescriptors
+        AZStd::vector<OpenXRInteractionComponentPathDescriptor> m_commonComponentPathDescriptors;
+
+    private:
+        AZStd::string GetEditorText();
+    };
+
+    //! This asset defines a list of Interaction Profile Descriptors.
+    //! The Engine only needs one of these assets, which is used to express
+    //! all the different interaction profiles (aka XR Headset Devices) that
+    //! are supported by OpenXR.
+    //! Basically this asset contains data as listed here:
+    //! https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#semantic-path-interaction-profiles
+    class OpenXRInteractionProfilesAsset final
+        : public AZ::Data::AssetData
+    {
+    public:
+        AZ_CLASS_ALLOCATOR(OpenXRInteractionProfilesAsset, AZ::SystemAllocator);
+        AZ_RTTI(OpenXRInteractionProfilesAsset, "{02555DCD-E363-42FB-935C-4E67CC3A1699}", AZ::Data::AssetData);
+        static void Reflect(AZ::ReflectContext* context);
+
+        static constexpr char s_assetTypeName[] = "OpenXR Interaction Profiles";
+        static constexpr char s_assetExtension[] = "xrprofiles";
+
+        const OpenXRInteractionProfileDescriptor* GetInteractionProfileDescriptor(const AZStd::string& profileName) const;
+        const AZStd::string& GetActionPathTypeStr(const AZStd::string& profileName, const AZStd::string& userPathName, const AZStd::string& componentPathName) const;
+
+        //! The asset is just a list of Interaction Profile descriptors.
+        AZStd::vector<OpenXRInteractionProfileDescriptor> m_interactionProfileDescriptors;
+    };
+
+    //! Custom asset handler that helps validate the content of the asset before allowing
+    //! it to be saved on disk.
+    class OpenXRInteractionProfilesAssetHandler final
+        : public AzFramework::GenericAssetHandler<OpenXRInteractionProfilesAsset>
+    {
+    public:
+        AZ_RTTI(OpenXRInteractionProfilesAssetHandler, "{1C4A27E9-6768-4C59-9582-2A01A0DEC1D0}", AzFramework::GenericAssetHandler<OpenXRInteractionProfilesAsset>);
+        AZ_CLASS_ALLOCATOR(OpenXRInteractionProfilesAssetHandler, AZ::SystemAllocator);
+    
+        static constexpr char LogName[] = "OpenXRInteractionProfilesAssetHandler";
+    
+        OpenXRInteractionProfilesAssetHandler();
+    
+        bool SaveAssetData(const AZ::Data::Asset<AZ::Data::AssetData>& asset, AZ::IO::GenericStream* stream) override;
+    };
+
+
+}// namespace OpenXRVk

+ 6 - 0
Gems/OpenXRVk/Code/Include/OpenXRVk/OpenXRVkSystemComponent.h

@@ -12,6 +12,9 @@
 #include <OpenXRVk/OpenXRVkInstance.h>
 #include <AzCore/Component/Component.h>
 
+#include <OpenXRVk/OpenXRVkInteractionProfilesAsset.h>
+#include <OpenXRVk/OpenXRVkActionSetsAsset.h>
+
 namespace OpenXRVk
 {
     //! This class is the component related to the vulkan backend of XR.
@@ -63,5 +66,8 @@ namespace OpenXRVk
 
     private:
         XR::Ptr<OpenXRVk::Instance> m_instance;
+
+        AZStd::unique_ptr<OpenXRInteractionProfilesAssetHandler> m_interactionProfilesAssetHandler;
+        AZStd::unique_ptr<OpenXRActionSetsAssetHandler> m_actionSetsAssetHandler;
     };
 }

+ 310 - 0
Gems/OpenXRVk/Code/Source/Builders/OpenXRVkAssetsBuilder.cpp

@@ -0,0 +1,310 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/StringFunc/StringFunc.h>
+#include <AssetBuilderSDK/SerializationDependencies.h>
+
+#include <OpenXRVk/OpenXRVkInteractionProfilesAsset.h>
+#include <OpenXRVk/OpenXRVkActionSetsAsset.h>
+#include <OpenXRVk/OpenXRVkAssetsValidator.h>
+
+#include "OpenXRVkAssetsBuilder.h"
+
+namespace OpenXRVkBuilders
+{
+    template<class AssetType>
+    static AZStd::unique_ptr<AssetType> LoadAssetAsUniquePtr(const AZStd::string& filePath)
+    {
+        AZ::ObjectStream::FilterDescriptor loadFilter = AZ::ObjectStream::FilterDescriptor(&AZ::Data::AssetFilterNoAssetLoading, AZ::ObjectStream::FILTERFLAG_IGNORE_UNKNOWN_CLASSES);
+        auto actionSetsAssetPtr = AZ::Utils::LoadObjectFromFile<AssetType>(filePath, nullptr, loadFilter);
+        if (!actionSetsAssetPtr)
+        {
+            return nullptr;
+        }
+        return AZStd::unique_ptr<AssetType>(actionSetsAssetPtr);
+    }
+
+
+    void OpenXRAssetsBuilder::CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) const
+    {
+        //! First get the extension 
+        constexpr bool includeDot = false;
+        AZStd::string fileExtension;
+        bool result = AZ::StringFunc::Path::GetExtension(request.m_sourceFile.c_str(), fileExtension, includeDot);
+        if (result && (fileExtension == OpenXRVk::OpenXRInteractionProfilesAsset::s_assetExtension))
+        {
+            CreateInteractionProfilesAssetJobs(request, response);
+            return;
+        }
+
+        if (result && (fileExtension == OpenXRVk::OpenXRActionSetsAsset::s_assetExtension))
+        {
+            CreateActionSetsAssetJobs(request, response);
+            return;
+        }
+
+        //! Unknown extension.
+        AZ_Error(LogName, false, "Unknown file extension [%s] for this builder. Source file [%s]", fileExtension.c_str(), request.m_sourceFile.c_str());
+        response.m_result = AssetBuilderSDK::CreateJobsResultCode::Failed;
+    }
+    
+    
+    void OpenXRAssetsBuilder::ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const
+    {
+        //! First get the extension 
+        constexpr bool includeDot = false;
+        AZStd::string fileExtension;
+        bool result = AZ::StringFunc::Path::GetExtension(request.m_sourceFile.c_str(), fileExtension, includeDot);
+        if (result && (fileExtension == OpenXRVk::OpenXRInteractionProfilesAsset::s_assetExtension))
+        {
+            ProcessInteractionProfilesAssetJob(request, response);
+            return;
+        }
+        if (result && (fileExtension == OpenXRVk::OpenXRActionSetsAsset::s_assetExtension))
+        {
+            ProcessActionSetsAssetJob(request, response);
+        }
+    }
+
+
+    /////////////////////////////////////////////////////////////////////////////////
+    // OpenXRInteractionProfilesAsset Support Begin
+    void OpenXRAssetsBuilder::CreateInteractionProfilesAssetJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) const
+    {
+        for (const AssetBuilderSDK::PlatformInfo& platformInfo : request.m_enabledPlatforms)
+        {
+            AssetBuilderSDK::JobDescriptor jobDescriptor;
+            // Very high priority because this asset is required to initialize the OpenXR runtime
+            // and initialize the I/O actions system.
+            jobDescriptor.m_priority = 1000;
+            jobDescriptor.m_critical = true;
+            jobDescriptor.m_jobKey = InteractionProfilesAssetJobKey;
+            jobDescriptor.SetPlatformIdentifier(platformInfo.m_identifier.c_str());
+            response.m_createJobOutputs.emplace_back(AZStd::move(jobDescriptor));
+        } // for all request.m_enabledPlatforms
+
+        response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
+    }
+
+    
+    void OpenXRAssetsBuilder::ProcessInteractionProfilesAssetJob([[maybe_unused]] const AssetBuilderSDK::ProcessJobRequest& request, [[maybe_unused]] AssetBuilderSDK::ProcessJobResponse& response) const
+    {
+        // Open the file, and make sure there's no redundant data, the OpenXR Paths are well formatted, etc.
+       auto interactionProfilesAssetPtr = LoadAssetAsUniquePtr<OpenXRVk::OpenXRInteractionProfilesAsset>(request.m_fullPath);
+       if (!interactionProfilesAssetPtr)
+       {
+           AZ_Error(LogName, false, "Failed to load interaction profile source asset [%s]", request.m_fullPath.c_str());
+           response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
+           return;
+       }
+
+       auto outcome = OpenXRVkAssetsValidator::ValidateInteractionProfilesAsset(*interactionProfilesAssetPtr.get());
+       if (!outcome.IsSuccess())
+       {
+           AZ_Error(LogName, false, "Invalid InteractionProfilesAsset [%s]. Reason:\n%s",
+               request.m_fullPath.c_str(), outcome.GetError().c_str());
+           response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
+           return;
+       }
+
+       // We keep exact same asset name and extension.
+       AZStd::string assetFileName;
+       AZ::StringFunc::Path::GetFullFileName(request.m_fullPath.c_str(), assetFileName);
+
+       // Construct product full path
+       AZStd::string assetOutputPath;
+       AzFramework::StringFunc::Path::ConstructFull(request.m_tempDirPath.c_str(), assetFileName.c_str(), assetOutputPath, true);
+
+       bool result = AZ::Utils::SaveObjectToFile(assetOutputPath, AZ::DataStream::ST_XML, interactionProfilesAssetPtr.get());
+       if (result == false)
+       {
+           AZ_Error(LogName, false, "Failed to save asset to %s", assetOutputPath.c_str());
+           response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
+           return;
+       }
+
+       AssetBuilderSDK::JobProduct jobProduct;
+       if (!AssetBuilderSDK::OutputObject(interactionProfilesAssetPtr.get(), assetOutputPath,
+           azrtti_typeid<OpenXRVk::OpenXRInteractionProfilesAsset>(),
+           aznumeric_cast<uint32_t>(0), jobProduct))
+       {
+           AZ_Error(LogName, false, "FIXME this message.");
+           response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
+           return;
+       }
+       response.m_outputProducts.emplace_back(AZStd::move(jobProduct));
+       response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
+    }
+    // OpenXRInteractionProfilesAsset Support End
+    /////////////////////////////////////////////////////////////////////////////////////
+
+
+    static AZStd::string GetInteractionProfileAssetSourcePath(const OpenXRVk::OpenXRActionSetsAsset& actionSetsAsset)
+    {
+        const auto& sourceUuid = actionSetsAsset.m_interactionProfilesAsset.GetId().m_guid;
+        bool foundSource = false;
+        AZ::Data::AssetInfo sourceAssetInfo;
+        AZStd::string sourceWatchFolder;
+        AzToolsFramework::AssetSystemRequestBus::BroadcastResult(foundSource, &AzToolsFramework::AssetSystemRequestBus::Events::GetSourceInfoBySourceUUID,
+            sourceUuid, sourceAssetInfo, sourceWatchFolder);
+        AZStd::string sourcePath;
+        if (foundSource)
+        {
+            constexpr bool caseInsensitive = false;
+            constexpr bool normalize = true;
+            AZ::StringFunc::Path::Join(sourceWatchFolder.c_str(), sourceAssetInfo.m_relativePath.c_str(), sourcePath, caseInsensitive, normalize);
+        }
+        return sourcePath;
+    }
+
+
+    void OpenXRAssetsBuilder::CreateActionSetsAssetJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) const
+    {
+        // Make sure the InteractionProfiles asset referenced in this ActionSets asset exists. and if so,
+        // also declare job dependency.
+        constexpr bool caseInsensitive = false;
+        constexpr bool normalize = true;
+        AZStd::string sourcePath;
+        AZ::StringFunc::Path::Join(request.m_watchFolder.c_str(), request.m_sourceFile.c_str(), sourcePath, caseInsensitive, normalize);
+        auto actionSetsAssetPtr = LoadAssetAsUniquePtr<OpenXRVk::OpenXRActionSetsAsset>(sourcePath);
+        if (!actionSetsAssetPtr)
+        {
+            AZ_Error(LogName, false, "Failed to load the ActionSets asset at path[%s].", request.m_sourceFile.c_str());
+            response.m_result = AssetBuilderSDK::CreateJobsResultCode::Failed;
+            return;
+        }
+
+        auto interactionProfileSourcePath = GetInteractionProfileAssetSourcePath(*actionSetsAssetPtr.get());
+        if (interactionProfileSourcePath.empty())
+        {
+            AZ_Error(LogName, false, "An ActionSets source asset requires a valid InteractionProfiles source asset.");
+            response.m_result = AssetBuilderSDK::CreateJobsResultCode::Failed;
+            return;
+        }
+        
+        for (const AssetBuilderSDK::PlatformInfo& platformInfo : request.m_enabledPlatforms)
+        {
+            AssetBuilderSDK::JobDescriptor jobDescriptor;
+            // Very high priority because this asset is required to initialize the OpenXR runtime
+            // and initialize the I/O actions system.
+            jobDescriptor.m_priority = 999;
+            jobDescriptor.m_critical = true;
+            jobDescriptor.m_jobKey = ActionSetsAssetJobKey;
+            jobDescriptor.SetPlatformIdentifier(platformInfo.m_identifier.c_str());
+
+            AssetBuilderSDK::SourceFileDependency sourceFileDependency{};
+            sourceFileDependency.m_sourceFileDependencyPath = interactionProfileSourcePath;
+            auto jobDependency = AssetBuilderSDK::JobDependency(InteractionProfilesAssetJobKey, platformInfo.m_identifier,
+                AssetBuilderSDK::JobDependencyType::Order, sourceFileDependency);
+            jobDescriptor.m_jobDependencyList.emplace_back(AZStd::move(jobDependency));
+
+            response.m_createJobOutputs.emplace_back(AZStd::move(jobDescriptor));
+        } // for all request.m_enabledPlatforms
+
+        response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
+    }
+
+
+    //! Each action in an actionSet has a "name" and a "localizedName". The "name" can never be empty, but
+    //! if "localizedName" is empty we automatically patch it as an identical copy of "name".
+    static void FixEmptyLocalizedNames(OpenXRVk::OpenXRActionSetsAsset& actionSetAsset)
+    {
+        for (auto& actionSetDescriptor : actionSetAsset.m_actionSetDescriptors)
+        {
+            if (actionSetDescriptor.m_localizedName.empty())
+            {
+                AZ_Printf(OpenXRAssetsBuilder::LogName, "ActionSet had empty LocalizedName. Taking new value of [%s]", actionSetDescriptor.m_name.c_str());
+                actionSetDescriptor.m_localizedName = actionSetDescriptor.m_name;
+            }
+            for (auto& actionDescriptor : actionSetDescriptor.m_actionDescriptors)
+            {
+                if (actionDescriptor.m_localizedName.empty())
+                {
+                    AZ_Printf(OpenXRAssetsBuilder::LogName, "Action in ActionSet [%s] had empty LocalizedName. Taking new value of [%s]",
+                        actionSetDescriptor.m_name.c_str(), actionDescriptor.m_name.c_str());
+                    actionDescriptor.m_localizedName = actionDescriptor.m_name;
+                }
+            }
+        }
+    }
+
+
+    void OpenXRAssetsBuilder::ProcessActionSetsAssetJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const
+    {
+        auto actionSetsAssetPtr = LoadAssetAsUniquePtr<OpenXRVk::OpenXRActionSetsAsset>(request.m_fullPath);
+        if (!actionSetsAssetPtr)
+        {
+            AZ_Error(LogName, false, "Failed to Load ActionsSet asset from File %s", request.m_fullPath.c_str());
+            response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
+            return;
+        }
+
+        FixEmptyLocalizedNames(*actionSetsAssetPtr.get());
+
+        // The Action Sets Asset contains an asset reference to the OpenXRInteractionProfilesAsset that was used
+        // to construct the data in it. Because we are running in a builder context, the OpenXRInteractionProfilesAsset
+        // is loaded with a null handle, BUT the AssetHint is valid and we'll use the AssetHint to discover
+        // the OpenXRInteractionProfilesAsset and load it manually.
+        auto interactionProfileSourcePath = GetInteractionProfileAssetSourcePath(*actionSetsAssetPtr.get());
+        if (interactionProfileSourcePath.empty())
+        {
+            AZ_Error(LogName, false, "An ActionSets source asset requires a valid InteractionProfiles source asset.");
+            response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
+            return;
+        }
+        auto interactionProfileAssetPtr = LoadAssetAsUniquePtr<OpenXRVk::OpenXRInteractionProfilesAsset>(interactionProfileSourcePath);
+        if (!interactionProfileAssetPtr)
+        {
+            AZ_Error(LogName, false, "Failed to Load InteractionProfiles asset from File %s", interactionProfileSourcePath.c_str());
+            response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
+            return;
+        }
+
+        auto outcome = OpenXRVkAssetsValidator::ValidateActionSetsAsset(*actionSetsAssetPtr.get(), *interactionProfileAssetPtr.get());
+        if (!outcome.IsSuccess())
+        {
+            AZ_Error(LogName, false, "Invalid source ActionSets content when using source InteractionProfiles asset file [%s]. Reason:\n%s",
+                interactionProfileSourcePath.c_str(), outcome.GetError().c_str());
+            response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
+            return;
+        }
+
+        // We keep exact same asset name and extension.
+        AZStd::string assetFileName;
+        AZ::StringFunc::Path::GetFullFileName(request.m_fullPath.c_str(), assetFileName);
+
+        // Construct product full path
+        AZStd::string assetOutputPath;
+        AzFramework::StringFunc::Path::ConstructFull(request.m_tempDirPath.c_str(), assetFileName.c_str(), assetOutputPath, true);
+
+        bool result = AZ::Utils::SaveObjectToFile(assetOutputPath, AZ::DataStream::ST_XML, actionSetsAssetPtr.get());
+        if (result == false)
+        {
+            AZ_Error(LogName, false, "Failed to save asset to %s", assetOutputPath.c_str());
+            response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
+            return;
+        }
+
+        // This step is very important, because it declares that this OpenXRActionsSetAsset depends on a OpenXRInteractionProfilesAsset.
+        // This will guarantee that when the OpenXRActionsSetAsset is loaded at runtime, the Asset Catalog will report OnAssetReady
+        // only after the OpenXRInteractionProfilesAsset is already fully loaded and ready.
+        AssetBuilderSDK::JobProduct jobProduct;
+        if (!AssetBuilderSDK::OutputObject(actionSetsAssetPtr.get(), assetOutputPath, azrtti_typeid<OpenXRVk::OpenXRActionSetsAsset>(),
+            aznumeric_cast<uint32_t>(0), jobProduct))
+        {
+            AZ_Error(LogName, false, "Failed to output asset jobs and runtime dependencies.");
+            response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
+            return;
+        }
+        response.m_outputProducts.emplace_back(AZStd::move(jobProduct));
+        response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
+    }
+
+    
+} // namespace OpenXRVkBuilders
+

+ 47 - 0
Gems/OpenXRVk/Code/Source/Builders/OpenXRVkAssetsBuilder.h

@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AssetBuilderSDK/AssetBuilderBusses.h>
+#include <AssetBuilderSDK/AssetBuilderSDK.h>
+
+namespace OpenXRVkBuilders
+{
+    class OpenXRAssetsBuilder final
+        : public AssetBuilderSDK::AssetBuilderCommandBus::Handler
+    {
+    public:
+        AZ_TYPE_INFO(OpenXRAssetsBuilder, "{1D053000-7799-459D-B99B-FF6AE6394BC1}");
+
+        static constexpr char LogName[] = "OpenXRAssetsBuilder";
+
+        static constexpr const char* InteractionProfilesAssetJobKey = "XR Interaction Profiles Asset";
+        static constexpr const char* ActionSetsAssetJobKey = "XR Action Sets Asset";
+    
+        OpenXRAssetsBuilder() = default;
+        ~OpenXRAssetsBuilder() = default;
+    
+        // Asset Builder Callback Functions
+        void CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) const;
+        void ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const;
+    
+        // AssetBuilderSDK::AssetBuilderCommandBus interface
+        void ShutDown() override { };
+  
+    private:
+        AZ_DISABLE_COPY_MOVE(OpenXRAssetsBuilder);
+
+        void CreateInteractionProfilesAssetJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) const;
+        void ProcessInteractionProfilesAssetJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const;
+
+        void CreateActionSetsAssetJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) const;
+        void ProcessActionSetsAssetJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const;
+    };
+
+} // namespace OpenXRVkBuilders

+ 78 - 0
Gems/OpenXRVk/Code/Source/Builders/OpenXRVkAssetsBuilderSystemComponent.cpp

@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AssetBuilderSDK/AssetBuilderSDK.h>
+#include <AssetBuilderSDK/AssetBuilderBusses.h>
+
+#include <OpenXRVk/OpenXRVkInteractionProfilesAsset.h>
+#include <OpenXRVk/OpenXRVkActionSetsAsset.h>
+
+#include "OpenXRVkAssetsBuilderSystemComponent.h"
+
+namespace OpenXRVkBuilders
+{
+    void OpenXRAssetsBuilderSystemComponent::Reflect(AZ::ReflectContext* context)
+    {
+        OpenXRVk::OpenXRInteractionProfilesAsset::Reflect(context);
+        OpenXRVk::OpenXRActionSetsAsset::Reflect(context);
+
+        if (AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serialize->Class<OpenXRAssetsBuilderSystemComponent, Component>()
+                ->Version(1)
+                ->Attribute(AZ::Edit::Attributes::SystemComponentTags, AZStd::vector<AZ::Crc32>({ AssetBuilderSDK::ComponentTags::AssetBuilder }))
+                ;
+        }
+    }
+    
+    void OpenXRAssetsBuilderSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
+    {
+        provided.push_back(AZ_CRC("OpenXRAssetsBuilderService"));
+    }
+    
+    void OpenXRAssetsBuilderSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
+    {
+        incompatible.push_back(AZ_CRC("OpenXRAssetsBuilderService"));
+    }
+    
+    void OpenXRAssetsBuilderSystemComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
+    {
+        (void)required;
+    }
+    
+    void OpenXRAssetsBuilderSystemComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent)
+    {
+        dependent.push_back(AZ_CRC("AssetCatalogService"));
+    }
+    
+    void OpenXRAssetsBuilderSystemComponent::Init()
+    {
+    }
+    
+    void OpenXRAssetsBuilderSystemComponent::Activate()
+    {    
+        // Register Shader Asset Builder
+        AssetBuilderSDK::AssetBuilderDesc assetBuilderDescriptor;
+        assetBuilderDescriptor.m_name = "OpenXR ActionSets Builder";
+        assetBuilderDescriptor.m_version = 1; // First versuib
+        assetBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern(AZStd::string::format("*.%s", OpenXRVk::OpenXRInteractionProfilesAsset::s_assetExtension), AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
+        assetBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern(AZStd::string::format("*.%s", OpenXRVk::OpenXRActionSetsAsset::s_assetExtension), AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
+        assetBuilderDescriptor.m_busId = azrtti_typeid<OpenXRAssetsBuilder>();
+        assetBuilderDescriptor.m_createJobFunction = AZStd::bind(&OpenXRAssetsBuilder::CreateJobs, &m_actionSetsAssetBuilder, AZStd::placeholders::_1, AZStd::placeholders::_2);
+        assetBuilderDescriptor.m_processJobFunction = AZStd::bind(&OpenXRAssetsBuilder::ProcessJob, &m_actionSetsAssetBuilder, AZStd::placeholders::_1, AZStd::placeholders::_2);
+    
+        m_actionSetsAssetBuilder.BusConnect(assetBuilderDescriptor.m_busId);
+        AssetBuilderSDK::AssetBuilderBus::Broadcast(&AssetBuilderSDK::AssetBuilderBus::Handler::RegisterBuilderInformation, assetBuilderDescriptor);
+    }
+    
+    void OpenXRAssetsBuilderSystemComponent::Deactivate()
+    {
+        m_actionSetsAssetBuilder.BusDisconnect();
+    }
+
+} // namespace AZ

+ 49 - 0
Gems/OpenXRVk/Code/Source/Builders/OpenXRVkAssetsBuilderSystemComponent.h

@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/Component/Component.h>
+
+#include "OpenXRVkAssetsBuilder.h"
+
+namespace OpenXRVkBuilders
+{        
+    class OpenXRAssetsBuilderSystemComponent
+        : public AZ::Component
+    {
+    public:
+        AZ_COMPONENT(OpenXRAssetsBuilderSystemComponent, "{B046B553-CAB4-4AE5-9192-5E002771979B}");
+    
+        static void Reflect(AZ::ReflectContext* context);
+    
+        static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
+        static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible);
+        static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required);
+        static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent);
+    
+    protected:
+        ////////////////////////////////////////////////////////////////////////
+        // AzslShaderBuilderRequestBus interface implementation
+    
+        ////////////////////////////////////////////////////////////////////////
+    
+        ////////////////////////////////////////////////////////////////////////
+        // AZ::Component interface implementation
+        void Init() override;
+        void Activate() override;
+        void Deactivate() override;
+        ////////////////////////////////////////////////////////////////////////
+    
+    
+    private:
+        OpenXRAssetsBuilder m_actionSetsAssetBuilder;
+    
+    };
+        
+} // namespace OpenXRVkBuilders

+ 47 - 0
Gems/OpenXRVk/Code/Source/Builders/OpenXRVkBuilderModule.cpp

@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/Memory/SystemAllocator.h>
+#include <AzCore/Module/Module.h>
+
+#include "OpenXRVkAssetsBuilderSystemComponent.h"
+
+namespace OpenXRVkBuilders
+{
+    class OpenXRBuilderModule final
+        : public AZ::Module
+    {
+    public:
+        AZ_RTTI(OpenXRBuilderModule, "{43370465-DBF1-44BB-968D-97C0B42F5EA0}", AZ::Module);
+        AZ_CLASS_ALLOCATOR(OpenXRBuilderModule, AZ::SystemAllocator);
+    
+        OpenXRBuilderModule()
+        {
+            // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here.
+            m_descriptors.insert(m_descriptors.end(), {
+                OpenXRAssetsBuilderSystemComponent::CreateDescriptor(),
+            });
+        }
+    
+        /**
+         * Add required SystemComponents to the SystemEntity.
+         */
+        AZ::ComponentTypeList GetRequiredSystemComponents() const override
+        {
+            return AZ::ComponentTypeList{
+                azrtti_typeid<OpenXRAssetsBuilderSystemComponent>(),
+            };
+        }
+    };
+} // namespace OpenXRVkBuilders
+
+#if defined(O3DE_GEM_NAME)
+AZ_DECLARE_MODULE_CLASS(AZ_JOIN(Gem_, O3DE_GEM_NAME, _Builders), OpenXRVkBuilders::OpenXRBuilderModule)
+#else
+AZ_DECLARE_MODULE_CLASS(Gem_OpenXRVk_Builders, OpenXRVkBuilders::OpenXRBuilderModule)
+#endif

+ 438 - 0
Gems/OpenXRVk/Code/Source/OpenXRVkActionSetsAsset.cpp

@@ -0,0 +1,438 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+//! Breadcrumb... Because this asset serializes a field of type AZ::Data::Asset<...>
+//! We need to include this file first to avoid the following compiler error:
+//! error C2027: use of undefined type 'AZ::SerializeGenericTypeInfo<ValueType>'
+//! error C3861: 'GetClassTypeId': identifier not found
+//! The error is triggered when calling ->Field("InteractionProfilesAsset", &OpenXRActionSetsAsset::m_interactionProfilesAsset)
+#include <AzCore/Asset/AssetSerializer.h>
+
+#include <OpenXRVk/OpenXRVkActionSetsAsset.h>
+#include <OpenXRVk/OpenXRVkAssetsValidator.h>
+
+namespace OpenXRVk
+{
+    namespace EditorInternal
+    {
+        // This static asset reference is only relevant when the user is creating an OpenXRActionSetsAsset with the
+        // Asset Editor. Because this variable is a singleton, creating two of these assets at the same time won't be possible.
+        // But this is not an issue because most likely all OpenXRActionSetsAsset always use the same OpenXRInteractionProfilesAsset.
+        static AZ::Data::Asset<OpenXRInteractionProfilesAsset> s_asset;
+        static constexpr char LogName[] = "EditorInternal::OpenXRInteractionProfilesAsset";
+
+        static void BlockingReloadAssetIfNotReady(AZ::Data::Asset<OpenXRInteractionProfilesAsset>& profileAsset)
+        {
+            if (!profileAsset.GetId().IsValid())
+            {
+                return;
+            }
+            if (!profileAsset.IsReady())
+            {
+                profileAsset.QueueLoad();
+                if (profileAsset.IsLoading())
+                {
+                    profileAsset.BlockUntilLoadComplete();
+                }
+            }
+        }
+
+
+        static void SetCurrentInteractionProfilesAsset(AZ::Data::Asset<OpenXRInteractionProfilesAsset>& newProfilesAsset,
+            bool loadAsset = true)
+        {
+            if (!newProfilesAsset.GetId().IsValid())
+            {
+                AZ_Printf(LogName, "The user cleared the global OpenXRInteractionProfilesAsset used for ActionSets Asset Editing.");
+                s_asset = {};
+                return;
+            }
+            if (loadAsset)
+            {
+                BlockingReloadAssetIfNotReady(newProfilesAsset);
+            }
+            s_asset = newProfilesAsset;
+        }
+
+
+        static const AZ::Data::Asset<OpenXRInteractionProfilesAsset>& GetCurrentInteractionProfilesAsset(bool loadAsset = true)
+        {
+            if (loadAsset)
+            {
+                BlockingReloadAssetIfNotReady(s_asset);
+            }
+            return s_asset;
+        }
+    }
+
+    ///////////////////////////////////////////////////////////
+    /// OpenXRActionPathDescriptor
+    void OpenXRActionPathDescriptor::Reflect(AZ::ReflectContext* context)
+    {
+        AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
+        if (serialize)
+        {
+            serialize->Class<OpenXRActionPathDescriptor>()
+                ->Version(1)
+                ->Field("InteractionProfile", &OpenXRActionPathDescriptor::m_interactionProfileName)
+                ->Field("UserPath", &OpenXRActionPathDescriptor::m_userPathName)
+                ->Field("ComponentPath", &OpenXRActionPathDescriptor::m_componentPathName)
+                ;
+
+            AZ::EditContext* edit = serialize->GetEditContext();
+            if (edit)
+            {
+                edit->Class<OpenXRActionPathDescriptor>("OpenXRActionPathDescriptor", "A specific OpenXR I/O action path.")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                    ->Attribute(AZ::Edit::Attributes::NameLabelOverride, &OpenXRActionPathDescriptor::GetEditorText)
+                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+                    ->DataElement(AZ::Edit::UIHandlers::ComboBox, &OpenXRActionPathDescriptor::m_interactionProfileName, "Interaction Profile", "The name of the Interaction Profile.")
+                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &OpenXRActionPathDescriptor::OnInteractionProfileSelected)
+                    ->Attribute(AZ::Edit::Attributes::StringList, &OpenXRActionPathDescriptor::GetInteractionProfiles)
+                    ->DataElement(AZ::Edit::UIHandlers::ComboBox, &OpenXRActionPathDescriptor::m_userPathName, "User Path", "Name of the User Path.")
+                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &OpenXRActionPathDescriptor::OnUserPathSelected)
+                    ->Attribute(AZ::Edit::Attributes::StringList, &OpenXRActionPathDescriptor::GetUserPaths)
+                    ->DataElement(AZ::Edit::UIHandlers::ComboBox, &OpenXRActionPathDescriptor::m_componentPathName, "I/O Component Path", "The name of I/O Component Path.")
+                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, &OpenXRActionPathDescriptor::OnComponentPathSelected)
+                    ->Attribute(AZ::Edit::Attributes::StringList, &OpenXRActionPathDescriptor::GetComponentPaths)
+                    ;
+            }
+        }
+    }
+
+
+    AZStd::string OpenXRActionPathDescriptor::GetEditorText() const
+    {
+        if (m_interactionProfileName.empty())
+        {
+            return "<Unknown Interaction Profile>";
+        }
+        if (m_userPathName.empty())
+        {
+            return "<Unknown User Path>";
+        }
+        if (m_componentPathName.empty())
+        {
+            return "<Unknown Component Path>";
+        }
+        return AZStd::string::format("%s %s %s", m_interactionProfileName.c_str(), m_userPathName.c_str(), m_componentPathName.c_str());
+    }
+
+
+    AZ::Crc32 OpenXRActionPathDescriptor::OnInteractionProfileSelected()
+    {
+        return AZ::Edit::PropertyRefreshLevels::AttributesAndValues;
+    }
+
+
+    AZStd::vector<AZStd::string> OpenXRActionPathDescriptor::GetInteractionProfiles() const
+    {
+        const auto& interactionProfilesAsset = EditorInternal::GetCurrentInteractionProfilesAsset();
+        if (!interactionProfilesAsset.IsReady())
+        {
+            return {};
+        }
+        AZStd::vector<AZStd::string> profileNames;
+        for (const auto& profileDescriptor : interactionProfilesAsset->m_interactionProfileDescriptors)
+        {
+            profileNames.push_back(profileDescriptor.m_name);
+        }
+        return profileNames;
+    }
+
+
+    AZ::Crc32 OpenXRActionPathDescriptor::OnUserPathSelected()
+    {
+        return AZ::Edit::PropertyRefreshLevels::AttributesAndValues;
+    }
+
+
+    AZStd::vector<AZStd::string> OpenXRActionPathDescriptor::GetUserPaths() const
+    {
+        const auto& interactionProfilesAsset = EditorInternal::GetCurrentInteractionProfilesAsset();
+        if (!interactionProfilesAsset.IsReady())
+        {
+            return {};
+        }
+
+        AZStd::vector<AZStd::string> retList;
+        const auto profileDescriptor = interactionProfilesAsset->GetInteractionProfileDescriptor(m_interactionProfileName);
+        if (!profileDescriptor)
+        {
+            return retList;
+        }
+        for (const auto& userPathDescriptor : profileDescriptor->m_userPathDescriptors)
+        {
+            retList.push_back(userPathDescriptor.m_name);
+        }
+        return retList;
+    }
+
+
+    AZ::Crc32 OpenXRActionPathDescriptor::OnComponentPathSelected()
+    {
+        return AZ::Edit::PropertyRefreshLevels::AttributesAndValues;
+    }
+
+
+    AZStd::vector<AZStd::string> OpenXRActionPathDescriptor::GetComponentPaths() const
+    {
+        const auto& interactionProfilesAsset = EditorInternal::GetCurrentInteractionProfilesAsset();
+        if (!interactionProfilesAsset.IsReady())
+        {
+            return {};
+        }
+
+        AZStd::vector<AZStd::string> retList;
+
+        const auto profileDescriptor = interactionProfilesAsset->GetInteractionProfileDescriptor(m_interactionProfileName);
+        if (!profileDescriptor)
+        {
+            return retList;
+        }
+
+        const auto* userPathDescriptor = profileDescriptor->GetUserPathDescriptor(m_userPathName);
+        if (!userPathDescriptor)
+        {
+            return retList;
+        }
+
+        for (const auto& componentPath : userPathDescriptor->m_componentPathDescriptors)
+        {
+            retList.push_back(componentPath.m_name);
+        }
+
+        for (const auto& componentPath : profileDescriptor->m_commonComponentPathDescriptors)
+        {
+            retList.push_back(componentPath.m_name);
+        }
+
+        return retList;
+    }
+    /// OpenXRActionPathDescriptor
+    ///////////////////////////////////////////////////////////
+
+
+    ///////////////////////////////////////////////////////////
+    /// OpenXRActionDescriptor
+    void OpenXRActionDescriptor::Reflect(AZ::ReflectContext* context)
+    {
+        OpenXRActionPathDescriptor::Reflect(context);
+
+        AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
+        if (serialize)
+        {
+            serialize->Class<OpenXRActionDescriptor>()
+                ->Version(1)
+                ->Field("Name", &OpenXRActionDescriptor::m_name)
+                ->Field("LocalizedName", &OpenXRActionDescriptor::m_localizedName)
+                ->Field("Comment", &OpenXRActionDescriptor::m_comment)
+                ->Field("ActionPathDescriptors", &OpenXRActionDescriptor::m_actionPathDescriptors)
+                ;
+
+            AZ::EditContext* edit = serialize->GetEditContext();
+            if (edit)
+            {
+                edit->Class<OpenXRActionDescriptor>("OpenXRActionDescriptor", "An action bound to one or more OpenXR Action Paths.")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                    ->Attribute(AZ::Edit::Attributes::NameLabelOverride, &OpenXRActionDescriptor::GetEditorText)
+                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRActionDescriptor::m_name, "Name", "Runtime identifier for this action.")
+                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshAttributesAndValues"))
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRActionDescriptor::m_localizedName, "Localized Name", "User friendly display name.")
+                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshAttributesAndValues"))
+                    ->DataElement(AZ::Edit::UIHandlers::MultiLineEdit, &OpenXRActionDescriptor::m_comment, "Comment", "Free form description of this Action Set.")
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRActionDescriptor::m_actionPathDescriptors, "Action Paths", "List of action paths bound to this action")
+                    ;
+            }
+        }
+    }
+
+
+    AZStd::string OpenXRActionDescriptor::GetEditorText() const
+    {
+        // In addition to showing the Action name, we'll append the Action Data Type
+        // of the first ActionPath, so it is easy for the developer to see what will be
+        // the expected data type at runtime.
+        AZStd::string actionTypeStr = "Unknown Type";
+        if (!m_actionPathDescriptors.empty())
+        {
+            if (EditorInternal::s_asset)
+            {
+                actionTypeStr = EditorInternal::s_asset->GetActionPathTypeStr(
+                    m_actionPathDescriptors[0].m_interactionProfileName,
+                    m_actionPathDescriptors[0].m_userPathName,
+                    m_actionPathDescriptors[0].m_componentPathName
+                );
+            }
+        }
+        AZStd::string actionDescription = AZStd::string::format("%s [%s]", m_name.empty() ? "<Unknown Action>" : m_name.c_str(), actionTypeStr.c_str());
+        return actionDescription;
+    }
+    /// OpenXRActionDescriptor
+    ///////////////////////////////////////////////////////////
+
+    ///////////////////////////////////////////////////////////
+    /// OpenXRActionSetDescriptor
+    void OpenXRActionSetDescriptor::Reflect(AZ::ReflectContext* context)
+    {
+        OpenXRActionDescriptor::Reflect(context);
+
+        AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
+        if (serialize)
+        {
+            serialize->Class<OpenXRActionSetDescriptor>()
+                ->Version(1)
+                ->Field("Name", &OpenXRActionSetDescriptor::m_name)
+                ->Field("LocalizedName", &OpenXRActionSetDescriptor::m_localizedName)
+                ->Field("Priority", &OpenXRActionSetDescriptor::m_priority)
+                ->Field("Comment", &OpenXRActionSetDescriptor::m_comment)
+                ->Field("ActionDescriptors", &OpenXRActionSetDescriptor::m_actionDescriptors)
+                ;
+
+            AZ::EditContext* edit = serialize->GetEditContext();
+            if (edit)
+            {
+                edit->Class<OpenXRActionSetDescriptor>("OpenXRActionSetDescriptor", "A group of OpenXR Actions that can be selectively activated/deactivated at runtime.")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                    ->Attribute(AZ::Edit::Attributes::NameLabelOverride, &OpenXRActionSetDescriptor::GetEditorText)
+                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRActionSetDescriptor::m_name, "Name", "Runtime identifier for this action set.")
+                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshAttributesAndValues"))
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRActionSetDescriptor::m_localizedName, "Localized Name", "Action set display name.")
+                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshAttributesAndValues"))
+                    ->DataElement(AZ::Edit::UIHandlers::SpinBox, &OpenXRActionSetDescriptor::m_priority, "Priority", "The higher this value the higher the priority.")
+                    ->DataElement(AZ::Edit::UIHandlers::MultiLineEdit, &OpenXRActionSetDescriptor::m_comment, "Comment", "Free form description of this Action Set.")
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRActionSetDescriptor::m_actionDescriptors, "Actions", "List of actions for this action set.")
+                    ;
+            }
+        }
+    }
+
+
+    AZStd::string OpenXRActionSetDescriptor::GetEditorText() const
+    {
+        if (!m_name.empty())
+        {
+            return m_name;
+        }
+        return m_localizedName.empty() ? "<Unknown Action Set>" : m_localizedName;
+    }
+    /// OpenXRActionSetDescriptor
+    ///////////////////////////////////////////////////////////
+    
+
+    ///////////////////////////////////////////////////////////
+    /// OpenXRActionBindingsAsset
+    void OpenXRActionSetsAsset::Reflect(AZ::ReflectContext* context)
+    {
+        OpenXRActionSetDescriptor::Reflect(context);
+
+        AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
+        if (serialize)
+        {
+            serialize->Class<OpenXRActionSetsAsset, AZ::Data::AssetData>()
+                ->Version(1)
+                ->Attribute(AZ::Edit::Attributes::EnableForAssetEditor, true)
+                ->Field("InteractionProfilesAsset", &OpenXRActionSetsAsset::m_interactionProfilesAsset)
+                ->Field("ActionSetDescriptors", &OpenXRActionSetsAsset::m_actionSetDescriptors)
+                ;
+
+            AZ::EditContext* edit = serialize->GetEditContext();
+            if (edit)
+            {
+                edit->Class<OpenXRActionSetsAsset>(
+                    s_assetTypeName, "Defines the OpenXR Actions an application cares about.")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                        ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRActionSetsAsset::m_interactionProfilesAsset, "Interaction Profiles Asset", "This asset determines the hardware systems that are compatible with these Action Sets.")
+                        ->Attribute(AZ::Edit::Attributes::ChangeNotify, &OpenXRActionSetsAsset::OnInteractionProfilesAssetChanged)
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRActionSetsAsset::m_actionSetDescriptors, "Action Sets", "List of action sets.")
+                    ;
+            }
+        }
+    }
+
+
+    AZ::Crc32 OpenXRActionSetsAsset::OnInteractionProfilesAssetChanged()
+    {
+        EditorInternal::SetCurrentInteractionProfilesAsset(m_interactionProfilesAsset);
+        return AZ::Edit::PropertyRefreshLevels::AttributesAndValues;
+    }
+    /// OpenXRActionBindingsAsset
+    ///////////////////////////////////////////////////////////
+    
+
+    ///////////////////////////////////////////////////////////
+    /// OpenXRActionBindingsAssetHandler
+    OpenXRActionSetsAssetHandler::OpenXRActionSetsAssetHandler()
+        : AzFramework::GenericAssetHandler<OpenXRActionSetsAsset>(
+            OpenXRActionSetsAsset::s_assetTypeName,
+            "Other",
+            OpenXRActionSetsAsset::s_assetExtension)
+    {
+    }
+    
+
+    AZ::Data::AssetHandler::LoadResult OpenXRActionSetsAssetHandler::LoadAssetData(
+        const AZ::Data::Asset<AZ::Data::AssetData>& asset,
+        AZStd::shared_ptr<AZ::Data::AssetDataStream> stream,
+        const AZ::Data::AssetFilterCB& assetLoadFilterCB)
+    {
+        auto actionSetsAsset = asset.GetAs<OpenXRActionSetsAsset>();
+        if (!actionSetsAsset)
+        {
+            AZ_Error("OpenXRActionSetsAssetHandler", false, "This should be a OpenXRActionSetsAsset, as this is the only type we process.");
+            return AZ::Data::AssetHandler::LoadResult::Error;
+        }
+    
+        if (!AZ::Utils::LoadObjectFromStreamInPlace(
+            *stream, *actionSetsAsset, nullptr, AZ::ObjectStream::FilterDescriptor(assetLoadFilterCB)))
+        {
+            return AZ::Data::AssetHandler::LoadResult::Error;
+        }
+
+        // The reason we don't load the interaction profiles asset upon construction
+        // is because we only want to load the singleton asset only if we are 100% sure
+        // this asset is being edited by the Asset Editor.
+        // Remember that at game runtime, there's no such thing as the Asset Editor.
+        constexpr bool loadAsset = false;
+        EditorInternal::SetCurrentInteractionProfilesAsset(actionSetsAsset->m_interactionProfilesAsset, loadAsset);
+    
+        return AZ::Data::AssetHandler::LoadResult::LoadComplete;
+    
+    }
+
+
+    bool OpenXRActionSetsAssetHandler::SaveAssetData(const AZ::Data::Asset<AZ::Data::AssetData>& asset, AZ::IO::GenericStream* stream)
+    {
+        OpenXRActionSetsAsset* assetData = asset.GetAs<OpenXRActionSetsAsset>();
+        if (!assetData->m_interactionProfilesAsset.GetId().IsValid())
+        {
+            AZ_Error("OpenXRActionSetsAssetHandler", false, "Can't save this OpenXRActionSetsAsset without a valid OpenXRInteractionProfilesAsset")
+            return false;
+        }
+
+        if (!assetData->m_interactionProfilesAsset.IsReady())
+        {
+            EditorInternal::SetCurrentInteractionProfilesAsset(assetData->m_interactionProfilesAsset);
+        }
+
+        auto outcome = OpenXRVkAssetsValidator::ValidateActionSetsAsset(*assetData, *(assetData->m_interactionProfilesAsset.Get()));
+        if (!outcome.IsSuccess())
+        {
+            AZ_Error("OpenXRActionSetsAssetHandler", false, "Can't save this OpenXRActionSetsAsset. Reason:\n%s", outcome.GetError().c_str());
+            return false;
+        }
+
+        return AzFramework::GenericAssetHandler<OpenXRActionSetsAsset>::SaveAssetData(asset, stream);
+    }
+    /// OpenXRActionBindingsAssetHandler
+    ///////////////////////////////////////////////////////////
+
+} // namespace OpenXRVk

+ 608 - 0
Gems/OpenXRVk/Code/Source/OpenXRVkAssetsValidator.cpp

@@ -0,0 +1,608 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/std/string/regex.h>
+
+#include <OpenXRVk/OpenXRVkAssetsValidator.h>
+
+namespace OpenXRVkAssetsValidator
+{
+    ///////////////////////////////////////////////////////////////////////////
+    // Asset Validation Common Start
+    // A regular Name is just a utf-8 string, it can contain upper case letters
+    // and spaces in between. But it can not be empty, and can not contain leading to trailing spaces.
+    static AZ::Outcome<void, AZStd::string> ValidateName(const AZStd::string& name)
+    {
+        if (name.empty())
+        {
+            return AZ::Failure("Name should not be empty.");
+        }
+        //Spaces at the beginning and end of the name are not allowed.
+        AZStd::string tmpName(name);
+        AZ::StringFunc::TrimWhiteSpace(tmpName, true, true);
+        if (tmpName.empty())
+        {
+            return AZ::Failure("Name is just a bunch of spaces.");
+        }
+        if (tmpName.size() != name.size())
+        {
+            return AZ::Failure(
+                AZStd::string::format("Trailing or leading spaces are not allowed in a Name [%s].",
+                    name.c_str())
+            );
+        }
+        return AZ::Success();
+    }
+
+
+    // Unlike an OpenXR Name, a Localized Name is just a utf-8 string, it can contain upper case letters
+    // and spaces in between. But it can not be empty, and can not contain leading to trailing spaces.
+    static AZ::Outcome<void, AZStd::string> ValidateOpenXRLocalizedName(const AZStd::string& name)
+    {
+        auto outcome = ValidateName(name);
+        if (!outcome.IsSuccess())
+        {
+            return AZ::Failure(
+                AZStd::string::format("Localized Name [%s] is invalid. Reason:\n%s", name.c_str(), outcome.GetError().c_str())
+            );
+        }
+        return AZ::Success();
+    }
+
+    // Asset Validation Common End
+    ///////////////////////////////////////////////////////////////////////////
+
+
+    ///////////////////////////////////////////////////////////////////////////
+    // OpenXRInteractionProfilesAsset Validation Start
+    static AZ::Outcome<void, AZStd::string> ValidateActionTypeString(const AZStd::string& actionTypeStr)
+    {
+        using CPD = OpenXRVk::OpenXRInteractionComponentPathDescriptor;
+        static const AZStd::unordered_set<AZStd::string> ValidActionTypes{
+            {CPD::s_TypeBoolStr},
+            {CPD::s_TypeFloatStr},
+            {CPD::s_TypeVector2Str},
+            {CPD::s_TypePoseStr},
+            {CPD::s_TypeVibrationStr}
+        };
+
+        if (!ValidActionTypes.contains(actionTypeStr))
+        {
+            static AZStd::string ValidListStr;
+            if (ValidListStr.empty())
+            {
+                ValidListStr += "[ ";
+                for (const auto& validActionTypeStr : ValidActionTypes)
+                {
+                    if (!ValidListStr.empty())
+                    {
+                        ValidListStr += ", ";
+                    }
+                    ValidListStr += validActionTypeStr;
+                }
+                ValidListStr += " ]";
+            }
+            return AZ::Failure(
+                AZStd::string::format("Action Type [%s] is invalid. It can only be one of %s",
+                    actionTypeStr.c_str(), ValidListStr.c_str())
+            );
+        }
+
+        return AZ::Success();
+    }
+
+
+    // An OpenXR path string only contain characters as described here
+    // https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#well-formed-path-strings
+    static AZ::Outcome<void, AZStd::string> ValidateOpenXRPath(const AZStd::string& path)
+    {
+        static AZStd::regex s_validCharactersRegEx(R"(^(/[a-z0-9\-_\.]+)+$)", AZStd::regex::ECMAScript);
+        if (!AZStd::regex_match(path, s_validCharactersRegEx))
+        {
+            return AZ::Failure(
+                AZStd::string::format("The path [%s] contains an invalid character, or is missing a leading '/' or contains a leading '/'", path.c_str())
+            );
+        }
+        return AZ::Success();
+    }
+
+
+    static AZ::Outcome<void, AZStd::string> ValidateComponentPathDescriptor(const OpenXRVk::OpenXRInteractionComponentPathDescriptor& componentPathDescriptor,
+        AZStd::unordered_set<AZStd::string>& uniqueComponentPathNames, AZStd::unordered_set<AZStd::string>& uniqueComponentPathPaths)
+    {
+        {
+            if (uniqueComponentPathNames.contains(componentPathDescriptor.m_name))
+            {
+                return AZ::Failure(
+                    AZStd::string::format("A Component Path with name [%s] already exists.",
+                        componentPathDescriptor.m_name.c_str())
+                );
+            }
+            uniqueComponentPathNames.emplace(componentPathDescriptor.m_name);
+            auto outcome = ValidateName(componentPathDescriptor.m_name);
+            if (!outcome.IsSuccess())
+            {
+                return AZ::Failure(
+                    AZStd::string::format("Component Name[%s] is invalid.Reason:\n%s", componentPathDescriptor.m_name.c_str(), outcome.GetError().c_str())
+                );
+            }
+        }
+        {
+            if (uniqueComponentPathPaths.contains(componentPathDescriptor.m_path))
+            {
+                return AZ::Failure(
+                    AZStd::string::format("A Component Path with path [%s] already exists.",
+                        componentPathDescriptor.m_path.c_str())
+                );
+            }
+            uniqueComponentPathPaths.emplace(componentPathDescriptor.m_path);
+            auto outcome = ValidateOpenXRPath(componentPathDescriptor.m_path);
+            if (!outcome.IsSuccess())
+            {
+                return AZ::Failure(
+                    AZStd::string::format("Component Path path [%s] is invalid. Reason:\n%s", componentPathDescriptor.m_path.c_str(), outcome.GetError().c_str())
+                );
+            }
+        }
+
+        auto outcome = ValidateActionTypeString(componentPathDescriptor.m_actionTypeStr);
+        if (!outcome.IsSuccess())
+        {
+            return AZ::Failure(
+                AZStd::string::format("Component Path path [%s] has an invalid action type. Reason:\n%s", componentPathDescriptor.m_name.c_str(), outcome.GetError().c_str())
+            );
+        }
+
+        return AZ::Success();
+    }
+
+
+    static AZ::Outcome<void, AZStd::string> ValidateUserPathDescriptor(const OpenXRVk::OpenXRInteractionUserPathDescriptor& userPathDescriptor,
+        AZStd::unordered_set<AZStd::string>& uniqueUserPathNames, AZStd::unordered_set<AZStd::string>& uniqueUserPathPaths,
+        AZStd::unordered_set<AZStd::string>& uniqueComponentPathNames, AZStd::unordered_set<AZStd::string>& uniqueComponentPathPaths)
+    {
+        {
+            if (uniqueUserPathNames.contains(userPathDescriptor.m_name))
+            {
+                return AZ::Failure(
+                    AZStd::string::format("An User Path with name [%s] already exists.",
+                        userPathDescriptor.m_name.c_str())
+                );
+            }
+            uniqueUserPathNames.emplace(userPathDescriptor.m_name);
+            auto outcome = ValidateName(userPathDescriptor.m_name);
+            if (!outcome.IsSuccess())
+            {
+                return AZ::Failure(
+                    AZStd::string::format("User Path Name [%s] is invalid. Reason:\n%s", userPathDescriptor.m_name.c_str(), outcome.GetError().c_str())
+                );
+            }
+        }
+        {
+            if (uniqueUserPathPaths.contains(userPathDescriptor.m_path))
+            {
+                return AZ::Failure(
+                    AZStd::string::format("An User Path with path [%s] already exists.",
+                        userPathDescriptor.m_path.c_str())
+                );
+            }
+            uniqueUserPathPaths.emplace(userPathDescriptor.m_path);
+            auto outcome = ValidateOpenXRPath(userPathDescriptor.m_path);
+            if (!outcome.IsSuccess())
+            {
+                return AZ::Failure(
+                    AZStd::string::format("User Path path [%s] is invalid. Reason:\n%s", userPathDescriptor.m_path.c_str(), outcome.GetError().c_str())
+                );
+            }
+        }
+        for (const auto& componentPathDescriptor : userPathDescriptor.m_componentPathDescriptors)
+        {
+            auto outcome = ValidateComponentPathDescriptor(componentPathDescriptor,
+                uniqueComponentPathNames, uniqueComponentPathPaths);
+            if (!outcome.IsSuccess())
+            {
+                return AZ::Failure(
+                    AZStd::string::format("Invalid Component Path [%s]. Reason:\n%s", componentPathDescriptor.m_name.c_str(), outcome.GetError().c_str())
+                );
+            }
+        }
+
+        return AZ::Success();
+    }
+
+
+    static AZ::Outcome<void, AZStd::string> ValidateInteractionProfileDescriptor(
+        const OpenXRVk::OpenXRInteractionProfileDescriptor& interactionProfileDescriptor,
+        AZStd::unordered_set<AZStd::string>& uniqueNames, AZStd::unordered_set<AZStd::string>& uniquePaths)
+    {
+        {
+            if (uniqueNames.contains(interactionProfileDescriptor.m_name))
+            {
+                return AZ::Failure(
+                    AZStd::string::format("An Interaction Profile with name [%s] already exists.",
+                        interactionProfileDescriptor.m_name.c_str())
+                );
+            }
+            uniqueNames.emplace(interactionProfileDescriptor.m_name);
+            auto outcome = ValidateName(interactionProfileDescriptor.m_name);
+            if (!outcome.IsSuccess())
+            {
+                return AZ::Failure(
+                    AZStd::string::format("Interaction Profile Unique Name [%s] is invalid. Reason:\n%s", interactionProfileDescriptor.m_name.c_str(), outcome.GetError().c_str())
+                );
+            }
+        }
+
+        {
+            if (uniquePaths.contains(interactionProfileDescriptor.m_path))
+            {
+                return AZ::Failure(
+                    AZStd::string::format("An Interaction Profile with path [%s] already exists.",
+                        interactionProfileDescriptor.m_path.c_str())
+                );
+            }
+            uniquePaths.emplace(interactionProfileDescriptor.m_path);
+            auto outcome = ValidateOpenXRPath(interactionProfileDescriptor.m_path);
+            if (!outcome.IsSuccess())
+            {
+                return AZ::Failure(
+                    AZStd::string::format("Interaction Profile Path [%s] is invalid. Reason:\n%s", interactionProfileDescriptor.m_path.c_str(), outcome.GetError().c_str())
+                );
+            }
+        }
+
+        AZStd::unordered_set<AZStd::string> uniqueUserPathNames;
+        AZStd::unordered_set<AZStd::string> uniqueUserPathPaths;
+        AZStd::unordered_set<AZStd::string> uniqueComponentPathNames;
+        AZStd::unordered_set<AZStd::string> uniqueComponentPathPaths;
+        for (const auto& userPathDescriptor : interactionProfileDescriptor.m_userPathDescriptors)
+        {
+            auto outcome = ValidateUserPathDescriptor(userPathDescriptor,
+                uniqueUserPathNames, uniqueUserPathPaths, uniqueComponentPathNames, uniqueComponentPathPaths);
+            if (!outcome.IsSuccess())
+            {
+                return AZ::Failure(
+                    AZStd::string::format("Invalid User Path [%s]. Reason:\n%s", userPathDescriptor.m_name.c_str(), outcome.GetError().c_str())
+                );
+            }
+        }
+
+        for (const auto& componentPathDescriptor : interactionProfileDescriptor.m_commonComponentPathDescriptors)
+        {
+            auto outcome = ValidateComponentPathDescriptor(componentPathDescriptor,
+                uniqueComponentPathNames, uniqueComponentPathPaths);
+            if (!outcome.IsSuccess())
+            {
+                return AZ::Failure(
+                    AZStd::string::format("Invalid Common Component Path [%s]. Reason:\n%s", componentPathDescriptor.m_name.c_str(), outcome.GetError().c_str())
+                );
+            }
+        }
+
+        return AZ::Success();
+    }
+
+
+    AZ::Outcome<void, AZStd::string> ValidateInteractionProfilesAsset(
+        const OpenXRVk::OpenXRInteractionProfilesAsset& interactionProfilesAsset)
+    {
+        if (interactionProfilesAsset.m_interactionProfileDescriptors.empty())
+        {
+            return AZ::Failure("An InteractionProfiles asset requires at least one Interaction Profile");
+        }
+
+        AZStd::unordered_set<AZStd::string> uniqueNames;
+        AZStd::unordered_set<AZStd::string> uniquePaths;
+        uint32_t i = 0;
+        for (const auto& interactionProfileDescriptor : interactionProfilesAsset.m_interactionProfileDescriptors)
+        {
+            auto outcome = ValidateInteractionProfileDescriptor(interactionProfileDescriptor,
+                uniqueNames, uniquePaths);
+            if (!outcome.IsSuccess())
+            {
+                return AZ::Failure(
+                    AZStd::string::format("InteractionProfile[%u] is invalid. Reason:\n%s",
+                        i, outcome.GetError().c_str())
+                );
+            }
+            i++;
+        }
+        return AZ::Success();
+    }
+    // OpenXRInteractionProfilesAsset Validation End
+    ///////////////////////////////////////////////////////////////////////////
+
+
+    ///////////////////////////////////////////////////////////////////////////
+    // OpenXRActionSetsAsset Validation Start
+    static AZ::Outcome<void, AZStd::string> ValidateActionPathDescriptor(const OpenXRVk::OpenXRActionPathDescriptor& actionPathDescriptor,
+        const OpenXRVk::OpenXRInteractionProfilesAsset& interactionProfilesAsset,
+        AZStd::unordered_set<AZStd::string>& uniqueActionPaths)
+    {
+        auto concatenatedActionPath = actionPathDescriptor.m_interactionProfileName
+            + actionPathDescriptor.m_userPathName
+            + actionPathDescriptor.m_componentPathName;
+        if (uniqueActionPaths.contains(concatenatedActionPath))
+        {
+            return AZ::Failure(
+                AZStd::string::format("An Action Path with profile[%s], userPath[%s], componentPath[%s] already exists.",
+                    actionPathDescriptor.m_interactionProfileName.c_str(),
+                    actionPathDescriptor.m_userPathName.c_str(),
+                    actionPathDescriptor.m_componentPathName.c_str())
+            );
+        }
+        uniqueActionPaths.emplace(AZStd::move(concatenatedActionPath));
+
+        if (actionPathDescriptor.m_interactionProfileName.empty())
+        {
+            return AZ::Failure(
+                AZStd::string::format("ActionPath Descriptor must have an InteractionProfile name.")
+            );
+        }
+        const auto interactionProfileDescriptorPtr = interactionProfilesAsset.GetInteractionProfileDescriptor(actionPathDescriptor.m_interactionProfileName);
+        if (!interactionProfileDescriptorPtr)
+        {
+            return AZ::Failure(
+                AZStd::string::format("Unknown Interaction Profile Descriptor named [%s].",
+                    actionPathDescriptor.m_interactionProfileName.c_str())
+            );
+        }
+
+        if (actionPathDescriptor.m_userPathName.empty())
+        {
+            return AZ::Failure(
+                AZStd::string::format("ActionPath Descriptor must have an UserPath name.")
+            );
+        }
+        const auto userPathDescriptorPtr = interactionProfileDescriptorPtr->GetUserPathDescriptor(actionPathDescriptor.m_userPathName);
+        if (!userPathDescriptorPtr)
+        {
+            return AZ::Failure(
+                AZStd::string::format("Unknown UserPath descriptor named [%s].",
+                    actionPathDescriptor.m_userPathName.c_str())
+            );
+        }
+
+        if (actionPathDescriptor.m_componentPathName.empty())
+        {
+            return AZ::Failure(
+                AZStd::string::format("ActionPath Descriptor must have a ComponentPath name.")
+            );
+        }
+        const auto componentPathDescriptorPtr = interactionProfileDescriptorPtr->GetComponentPathDescriptor(*userPathDescriptorPtr, actionPathDescriptor.m_componentPathName);
+        if (!componentPathDescriptorPtr)
+        {
+            return AZ::Failure(
+                AZStd::string::format("Unknown ComponentPath descriptor named [%s].",
+                    actionPathDescriptor.m_componentPathName.c_str())
+            );
+        }
+
+        return AZ::Success();
+    }
+
+
+    static const AZStd::string& GetActionTypeStringFromActionPathDescriptor(
+        const OpenXRVk::OpenXRInteractionProfilesAsset& interactionProfilesAsset,
+        const OpenXRVk::OpenXRActionPathDescriptor& actionPathDescriptor
+    )
+    {
+        return interactionProfilesAsset.GetActionPathTypeStr(
+            actionPathDescriptor.m_interactionProfileName,
+            actionPathDescriptor.m_userPathName,
+            actionPathDescriptor.m_componentPathName
+        );
+    }
+
+
+    static bool IsActionTypeBoolOrFloat(const AZStd::string& actionTypeStr)
+    {
+        return (
+            (actionTypeStr == OpenXRVk::OpenXRInteractionComponentPathDescriptor::s_TypeBoolStr) ||
+            (actionTypeStr == OpenXRVk::OpenXRInteractionComponentPathDescriptor::s_TypeFloatStr)
+            );
+    }
+
+
+    static bool AreCompatibleActionTypeStrings(const AZStd::string& lhs, const AZStd::string& rhs)
+    {
+        if (IsActionTypeBoolOrFloat(lhs) && IsActionTypeBoolOrFloat(rhs))
+        {
+            return true;
+        }
+        return (lhs == rhs);
+    }
+
+
+    // An OpenXR name string only contain characters which are allowed in a SINGLE LEVEL of a well-formed path string
+    // https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#well-formed-path-strings
+    static AZ::Outcome<void, AZStd::string> ValidateOpenXRName(const AZStd::string& name)
+    {
+        static AZStd::regex s_validCharactersRegEx(R"(^[a-z0-9\-_\.]+$)", AZStd::regex::ECMAScript);
+        if (!AZStd::regex_match(name, s_validCharactersRegEx))
+        {
+            return AZ::Failure(
+                AZStd::string::format("The name [%s] contains an invalid character", name.c_str())
+            );
+        }
+        return AZ::Success();
+    }
+
+
+    static AZ::Outcome<void, AZStd::string> ValidateActionDescriptor(
+        const OpenXRVk::OpenXRInteractionProfilesAsset& interactionProfilesAsset,
+        const OpenXRVk::OpenXRActionDescriptor& actionDescriptor,
+        AZStd::unordered_set<AZStd::string>& uniqueActionNames,
+        AZStd::unordered_set<AZStd::string>& uniqueActionLocalizedNames)
+    {
+        {
+            if (uniqueActionNames.contains(actionDescriptor.m_name))
+            {
+                return AZ::Failure(
+                    AZStd::string::format("An Action with name [%s] already exists.",
+                        actionDescriptor.m_name.c_str())
+                );
+            }
+            uniqueActionNames.emplace(actionDescriptor.m_name);
+            auto outcome = ValidateOpenXRName(actionDescriptor.m_name);
+            if (!outcome.IsSuccess())
+            {
+                return AZ::Failure(
+                    AZStd::string::format("Failed to validate Action Descriptor named=[%s].\nReason:\n%s",
+                        actionDescriptor.m_name.c_str(), outcome.GetError().c_str())
+                );
+            }
+        }
+
+        // Only validate if not empty. If empty, the asset builder will force this to be a copy of
+        // actionDescriptor.m_name.
+        if (!actionDescriptor.m_localizedName.empty())
+        {
+            if (uniqueActionLocalizedNames.contains(actionDescriptor.m_localizedName))
+            {
+                return AZ::Failure(
+                    AZStd::string::format("An Action with localized name [%s] already exists.",
+                        actionDescriptor.m_localizedName.c_str())
+                );
+            }
+            uniqueActionLocalizedNames.emplace(actionDescriptor.m_localizedName);
+            auto outcome = ValidateOpenXRLocalizedName(actionDescriptor.m_localizedName);
+            if (!outcome.IsSuccess())
+            {
+                return AZ::Failure(
+                    AZStd::string::format("Failed to validate localized name of Action Descriptor named=[%s]\nReason:\n%s",
+                        actionDescriptor.m_name.c_str(), outcome.GetError().c_str())
+                );
+            }
+        }
+
+
+        if (actionDescriptor.m_actionPathDescriptors.empty())
+        {
+            return AZ::Failure(
+                AZStd::string::format("At least one ActionPath Descriptor is required by Action Descriptor named=[%s].\n",
+                    actionDescriptor.m_name.c_str())
+            );
+        }
+
+        AZStd::unordered_set<AZStd::string> uniqueActionPaths;
+
+        // It is very important that all action path descriptors have compatible data types.
+        const AZStd::string& firstActionTypeStr = GetActionTypeStringFromActionPathDescriptor(
+            interactionProfilesAsset, actionDescriptor.m_actionPathDescriptors[0]);
+        uint32_t actionPathIndex = 0;
+        for (const auto& actionPathDescriptor : actionDescriptor.m_actionPathDescriptors)
+        {
+            auto outcome = ValidateActionPathDescriptor(actionPathDescriptor, interactionProfilesAsset, uniqueActionPaths);
+            if (!outcome.IsSuccess())
+            {
+                return AZ::Failure(
+                    AZStd::string::format("Failed to validate Action Path Descriptor for Action Descriptor named=[%s].\nReason:\n%s",
+                        actionDescriptor.m_name.c_str(), outcome.GetError().c_str())
+                );
+            }
+            const AZStd::string& actionTypeStr = GetActionTypeStringFromActionPathDescriptor(
+                interactionProfilesAsset, actionPathDescriptor);
+            if (!AreCompatibleActionTypeStrings(firstActionTypeStr, actionTypeStr))
+            {
+                return AZ::Failure(
+                    AZStd::string::format("ActionType=[%s] of ActionPath Descriptor[%u] is NOT compatible with the ActionType=[%s] ActionPath Descriptor[0]",
+                        actionTypeStr.c_str(), actionPathIndex, firstActionTypeStr.c_str())
+                );
+            }
+            actionPathIndex++;
+        }
+
+        return AZ::Success();
+    }
+
+
+    AZ::Outcome<void, AZStd::string> ValidateActionSetsAsset(const OpenXRVk::OpenXRActionSetsAsset& actionSetsAsset,
+        const OpenXRVk::OpenXRInteractionProfilesAsset& interactionProfilesAsset)
+    {
+        if (actionSetsAsset.m_actionSetDescriptors.empty())
+        {
+            return AZ::Failure("At least one ActionSet must be listed in an ActionSets asset");
+        }
+
+        AZStd::unordered_set<AZStd::string> uniqueActionSetNames;
+        AZStd::unordered_set<AZStd::string> uniqueActionSetLocalizedNames;
+        for (const auto& actionSetDescriptor : actionSetsAsset.m_actionSetDescriptors)
+        {
+            {
+                if (uniqueActionSetNames.contains(actionSetDescriptor.m_name))
+                {
+                    return AZ::Failure(
+                        AZStd::string::format("An ActionSet named=[%s] already exists.",
+                            actionSetDescriptor.m_name.c_str())
+                    );
+                }
+                uniqueActionSetNames.emplace(actionSetDescriptor.m_name);
+                auto outcome = ValidateOpenXRName(actionSetDescriptor.m_name);
+                if (!outcome.IsSuccess())
+                {
+                    return AZ::Failure(
+                        AZStd::string::format("Failed to validate ActionSet Descriptor name=[%s]. Reason:\n%s",
+                            actionSetDescriptor.m_name.c_str(), outcome.GetError().c_str())
+                    );
+                }
+            }
+
+            // Only validate if not empty. If empty, the asset builder will force this to be a copy of
+            // actionSetDescriptor.m_name.
+            if (!actionSetDescriptor.m_localizedName.empty())
+            {
+                if (uniqueActionSetLocalizedNames.contains(actionSetDescriptor.m_localizedName))
+                {
+                    return AZ::Failure(
+                        AZStd::string::format("An ActionSet with localized named=[%s] already exists.",
+                            actionSetDescriptor.m_localizedName.c_str())
+                    );
+                }
+                uniqueActionSetLocalizedNames.emplace(actionSetDescriptor.m_localizedName);
+                auto outcome = ValidateOpenXRLocalizedName(actionSetDescriptor.m_localizedName);
+                if (!outcome.IsSuccess())
+                {
+                    return AZ::Failure(
+                        AZStd::string::format("Failed to validate ActionSet Descriptor name=[%s]. Reason:\n%s",
+                            actionSetDescriptor.m_name.c_str(), outcome.GetError().c_str())
+                    );
+                }
+            }
+
+            if (actionSetDescriptor.m_actionDescriptors.empty())
+            {
+                return AZ::Failure(
+                    AZStd::string::format("ActionSet [%s] must contain at least one ActionDescriptor.",
+                        actionSetDescriptor.m_name.c_str())
+                );
+            }
+
+            AZStd::unordered_set<AZStd::string> uniqueActionNames;
+            AZStd::unordered_set<AZStd::string> uniqueActionLocalizedNames;
+            for (const auto& actionDescriptor : actionSetDescriptor.m_actionDescriptors)
+            {
+                auto outcome = ValidateActionDescriptor(interactionProfilesAsset, actionDescriptor,
+                    uniqueActionNames, uniqueActionLocalizedNames);
+                if (!outcome.IsSuccess())
+                {
+                    return AZ::Failure(
+                        AZStd::string::format("Failed to validate ActionSet Descriptor name=[%s]. Reason:\n%s",
+                            actionSetDescriptor.m_name.c_str(), outcome.GetError().c_str())
+                    );
+                }
+            }
+        }
+
+        return AZ::Success();
+    }
+    // OpenXRActionSetsAsset Validation End
+    ///////////////////////////////////////////////////////////////////////////
+
+
+} // namespace OpenXRVkAssetsValidator

+ 355 - 0
Gems/OpenXRVk/Code/Source/OpenXRVkInteractionProfilesAsset.cpp

@@ -0,0 +1,355 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <OpenXRVk/OpenXRVkInteractionProfilesAsset.h>
+#include <OpenXRVk/OpenXRVkAssetsValidator.h>
+
+namespace OpenXRVk
+{
+    ///////////////////////////////////////////////////////////
+    /// OpenXRInteractionComponentPathDescriptor
+    XrActionType OpenXRInteractionComponentPathDescriptor::GetXrActionType(AZStd::string_view actionTypeStr)
+    {
+        if (actionTypeStr == s_TypeBoolStr)
+        {
+            return XR_ACTION_TYPE_BOOLEAN_INPUT;
+        }
+        else if (actionTypeStr == s_TypeFloatStr)
+        {
+            return XR_ACTION_TYPE_FLOAT_INPUT;
+        }
+        else if (actionTypeStr == s_TypeVector2Str)
+        {
+            return XR_ACTION_TYPE_VECTOR2F_INPUT;
+        }
+        else if (actionTypeStr == s_TypePoseStr)
+        {
+            return XR_ACTION_TYPE_POSE_INPUT;
+        }
+        else if (actionTypeStr == s_TypeVibrationStr)
+        {
+            return XR_ACTION_TYPE_VIBRATION_OUTPUT;
+        }
+        return XR_ACTION_TYPE_MAX_ENUM;
+    }
+
+
+    XrActionType OpenXRInteractionComponentPathDescriptor::GetXrActionType() const
+    {
+        return GetXrActionType(m_actionTypeStr);
+    }
+
+
+    static AZStd::vector<AZStd::string> GetEditorXrActionTypeNames()
+    {
+        static AZStd::vector<AZStd::string> s_actionTypeNames = {
+                {OpenXRInteractionComponentPathDescriptor::s_TypeBoolStr   },
+                {OpenXRInteractionComponentPathDescriptor::s_TypeFloatStr  },
+                {OpenXRInteractionComponentPathDescriptor::s_TypeVector2Str},
+                {OpenXRInteractionComponentPathDescriptor::s_TypePoseStr   },
+                {OpenXRInteractionComponentPathDescriptor::s_TypeVibrationStr   },
+        };
+        return s_actionTypeNames;
+    }
+
+
+    void OpenXRInteractionComponentPathDescriptor::Reflect(AZ::ReflectContext* context)
+    {
+        AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
+        if (serialize)
+        {
+            serialize->Class<OpenXRInteractionComponentPathDescriptor>()
+                ->Version(1)
+                ->Field("Name", &OpenXRInteractionComponentPathDescriptor::m_name)
+                ->Field("Path", &OpenXRInteractionComponentPathDescriptor::m_path)
+                ->Field("ActionType", &OpenXRInteractionComponentPathDescriptor::m_actionTypeStr)
+                ;
+
+            AZ::EditContext* edit = serialize->GetEditContext();
+            if (edit)
+            {
+                edit->Class<OpenXRInteractionComponentPathDescriptor>("Component Path", "An OpenXR Component Path that is supported by an OpenXR User Path")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                    ->Attribute(AZ::Edit::Attributes::NameLabelOverride, &OpenXRInteractionComponentPathDescriptor::GetEditorText)
+                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRInteractionComponentPathDescriptor::m_name, "Name", "User friendly name.")
+                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::AttributesAndValues)
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRInteractionComponentPathDescriptor::m_path, "Path", "An OpenXR Path string that starts with '/' BUT is relative to a User Path.")
+                    ->DataElement(AZ::Edit::UIHandlers::ComboBox, &OpenXRInteractionComponentPathDescriptor::m_actionTypeStr, "Action Type", "Data type of this action.")
+                    ->Attribute(AZ::Edit::Attributes::StringList, &GetEditorXrActionTypeNames)
+                    ;
+            }
+        }
+    }
+
+
+    AZStd::string OpenXRInteractionComponentPathDescriptor::GetEditorText()
+    {
+        return m_name.empty() ? "<Unknown Component Path>" : m_name;
+    }
+
+    /// OpenXRInteractionComponentPathDescriptor
+    ///////////////////////////////////////////////////////////
+
+
+    ///////////////////////////////////////////////////////////
+    /// OpenXRInteractionUserPathDescriptor
+    void OpenXRInteractionUserPathDescriptor::Reflect(AZ::ReflectContext* context)
+    {
+        AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
+        if (serialize)
+        {
+            serialize->Class<OpenXRInteractionUserPathDescriptor>()
+                ->Version(1)
+                ->Field("Name", &OpenXRInteractionUserPathDescriptor::m_name)
+                ->Field("Path", &OpenXRInteractionUserPathDescriptor::m_path)
+                ->Field("ComponentPaths", &OpenXRInteractionUserPathDescriptor::m_componentPathDescriptors)
+                ;
+
+            AZ::EditContext* edit = serialize->GetEditContext();
+            if (edit)
+            {
+                edit->Class<OpenXRInteractionUserPathDescriptor>("User Path", "Represents a User Path supported by an Interaction Profile")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                    ->Attribute(AZ::Edit::Attributes::NameLabelOverride, &OpenXRInteractionUserPathDescriptor::GetEditorText)
+                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRInteractionUserPathDescriptor::m_name, "Name", "User friendly name.")
+                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::AttributesAndValues)
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRInteractionUserPathDescriptor::m_path, "Path", "An OpenXR Path string that starts with '/'.")
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRInteractionUserPathDescriptor::m_componentPathDescriptors, "Component Paths", "List of component paths supported by this User Path")
+                    ;
+            }
+        }
+    }
+
+
+    AZStd::string OpenXRInteractionUserPathDescriptor::GetEditorText()
+    {
+        return m_name.empty() ? "<Unknown User Path>" : m_name;
+    }
+
+
+    const OpenXRInteractionComponentPathDescriptor* OpenXRInteractionUserPathDescriptor::GetComponentPathDescriptor(const AZStd::string& componentPathName) const
+    {
+        for (const auto& componentPathDescriptor : m_componentPathDescriptors)
+        {
+            if (componentPathDescriptor.m_name == componentPathName)
+            {
+                return &componentPathDescriptor;
+            }
+        }
+        return nullptr;
+    }
+
+    /// OpenXRInteractionUserPathDescriptor
+    ///////////////////////////////////////////////////////////
+
+
+    ///////////////////////////////////////////////////////////
+    /// OpenXRInteractionProfileDescriptor
+    void OpenXRInteractionProfileDescriptor::Reflect(AZ::ReflectContext* context)
+    {
+        OpenXRInteractionComponentPathDescriptor::Reflect(context);
+        OpenXRInteractionUserPathDescriptor::Reflect(context);
+
+        AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
+        if (serialize)
+        {
+            serialize->Class<OpenXRInteractionProfileDescriptor>()
+                ->Version(1)
+                ->Field("UniqueName", &OpenXRInteractionProfileDescriptor::m_name)
+                ->Field("Path", &OpenXRInteractionProfileDescriptor::m_path)
+                ->Field("UserPathDescriptors", &OpenXRInteractionProfileDescriptor::m_userPathDescriptors)
+                ->Field("CommonComponentPathDescriptors", &OpenXRInteractionProfileDescriptor::m_commonComponentPathDescriptors)
+                ;
+
+            AZ::EditContext* edit = serialize->GetEditContext();
+            if (edit)
+            {
+                edit->Class<OpenXRInteractionProfileDescriptor>(
+                    "Interaction Profile", "Defines an OpenXR Interaction Profile Supported by O3DE.")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                    ->Attribute(AZ::Edit::Attributes::NameLabelOverride, &OpenXRInteractionProfileDescriptor::GetEditorText)
+                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRInteractionProfileDescriptor::m_name, "Unique Name", "Unique name across all interaction profiles")
+                    ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::AttributesAndValues)
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRInteractionProfileDescriptor::m_path, "Path", "OpenXR Canonical Path for this interation profile.")
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRInteractionProfileDescriptor::m_userPathDescriptors, "User Paths", "List of user paths")
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRInteractionProfileDescriptor::m_commonComponentPathDescriptors, "Common Component Paths", "List of component paths supported by all User Paths")
+                    ;
+            }
+        }
+    }
+
+
+    AZStd::string OpenXRInteractionProfileDescriptor::GetEditorText()
+    {
+        return m_name.empty() ? "<Unknown Profile>" : m_name;
+    }
+
+
+    const OpenXRInteractionUserPathDescriptor* OpenXRInteractionProfileDescriptor::GetUserPathDescriptor(const AZStd::string& userPathName) const
+    {
+        for (const auto& userPathDescriptor : m_userPathDescriptors)
+        {
+            if (userPathDescriptor.m_name == userPathName)
+            {
+                return &userPathDescriptor;
+            }
+        }
+        return nullptr;
+    }
+
+
+    const OpenXRInteractionComponentPathDescriptor* OpenXRInteractionProfileDescriptor::GetCommonComponentPathDescriptor(const AZStd::string& componentPathName) const
+    {
+        for (const auto& componentPathDescriptor : m_commonComponentPathDescriptors)
+        {
+            if (componentPathDescriptor.m_name == componentPathName)
+            {
+                return &componentPathDescriptor;
+            }
+        }
+        return nullptr;
+    }
+
+
+    const OpenXRInteractionComponentPathDescriptor* OpenXRInteractionProfileDescriptor::GetComponentPathDescriptor(const OpenXRInteractionUserPathDescriptor& userPathDescriptor,
+        const AZStd::string& componentPathName) const
+    {
+        auto componentPathDescriptor = userPathDescriptor.GetComponentPathDescriptor(componentPathName);
+        if (!componentPathDescriptor)
+        {
+            // Look in common paths
+            return GetCommonComponentPathDescriptor(componentPathName);
+        }
+        return componentPathDescriptor;
+    }
+
+
+    AZStd::string OpenXRInteractionProfileDescriptor::GetComponentAbsolutePath(const OpenXRInteractionUserPathDescriptor& userPathDescriptor,
+        const AZStd::string& componentPathName) const
+    {
+        // First check if the user path owns the component path, if not, search in the common components list.
+        auto componentPathDescriptor = GetComponentPathDescriptor(userPathDescriptor, componentPathName);
+        if (!componentPathDescriptor)
+        {
+            return {};
+        }
+        return userPathDescriptor.m_path + componentPathDescriptor->m_path;
+    }
+    /// OpenXRInteractionProfileDescriptor
+    ///////////////////////////////////////////////////////////
+
+
+    ///////////////////////////////////////////////////////////
+    /// OpenXRInteractionProfilesAsset
+    void OpenXRInteractionProfilesAsset::Reflect(AZ::ReflectContext* context)
+    {
+        OpenXRInteractionProfileDescriptor::Reflect(context);
+
+        AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
+        if (serialize)
+        {
+            serialize->Class<OpenXRInteractionProfilesAsset, AZ::Data::AssetData>()
+                ->Version(1)
+                ->Attribute(AZ::Edit::Attributes::EnableForAssetEditor, true)
+                ->Field("InteractionProfiles", &OpenXRInteractionProfilesAsset::m_interactionProfileDescriptors)
+                ;
+
+            AZ::EditContext* edit = serialize->GetEditContext();
+            if (edit)
+            {
+                edit->Class<OpenXRInteractionProfilesAsset>(
+                    s_assetTypeName, "Defines the OpenXR Interaction Profiles supported by O3DE.")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &OpenXRInteractionProfilesAsset::m_interactionProfileDescriptors, "Interaction Profiles", "List of interaction profile descriptors.")
+                    ;
+            }
+        }
+    }
+
+
+    const OpenXRInteractionProfileDescriptor* OpenXRInteractionProfilesAsset::GetInteractionProfileDescriptor(const AZStd::string& profileName) const
+    {
+        for (const auto& profileDescriptor : m_interactionProfileDescriptors)
+        {
+            if (profileName == profileDescriptor.m_name)
+            {
+                return &profileDescriptor;
+            }
+        }
+        return nullptr;
+    }
+
+
+    const AZStd::string& OpenXRInteractionProfilesAsset::GetActionPathTypeStr(const AZStd::string& profileName, const AZStd::string& userPathName, const AZStd::string& componentPathName) const
+    {
+        static const AZStd::string emptyStr;
+        const auto profileDescriptor = GetInteractionProfileDescriptor(profileName);
+        if (!profileDescriptor)
+        {
+            return emptyStr;
+        }
+        const auto userPathDescriptor = profileDescriptor->GetUserPathDescriptor(userPathName);
+        if (!userPathDescriptor)
+        {
+            return emptyStr;
+        }
+        const auto componentPathDescriptor = profileDescriptor->GetComponentPathDescriptor(*userPathDescriptor, componentPathName);
+        if (!componentPathDescriptor)
+        {
+            return emptyStr;
+        }
+        return componentPathDescriptor->m_actionTypeStr;
+    }
+
+    /// OpenXRInteractionProfilesAsset
+    ///////////////////////////////////////////////////////////
+
+
+    ///////////////////////////////////////////////////////////
+    /// OpenXRInteractionProfilesAssetHandler
+    OpenXRInteractionProfilesAssetHandler::OpenXRInteractionProfilesAssetHandler()
+        : AzFramework::GenericAssetHandler<OpenXRInteractionProfilesAsset>(
+            OpenXRInteractionProfilesAsset::s_assetTypeName,
+            "Other",
+            OpenXRInteractionProfilesAsset::s_assetExtension)
+    {
+    }
+    
+    bool OpenXRInteractionProfilesAssetHandler::SaveAssetData(const AZ::Data::Asset<AZ::Data::AssetData>& asset, AZ::IO::GenericStream* stream)
+    {
+        auto profileAsset = asset.GetAs<OpenXRInteractionProfilesAsset>();
+        if (!profileAsset)
+        {
+            AZ_Error(LogName, false, "This should be an OpenXR Interaction Profile Asset, as this is the only type this handler can process.");
+            return false;
+        }
+        
+        auto outcome = OpenXRVkAssetsValidator::ValidateInteractionProfilesAsset(*profileAsset);
+        if (!outcome.IsSuccess())
+        {
+            AZ_Error(LogName, false, "Can't save this interaction profiles asset. Reason:\n%s\n", outcome.GetError().c_str());
+            return false;
+        }
+    
+        if (!m_serializeContext)
+        {
+            AZ_Error(LogName, false, "Can't save the OpenXR Interaction Profile Asset without a serialize context.");
+            return false;
+        }
+    
+        return AZ::Utils::SaveObjectToStream(*stream, AZ::ObjectStream::ST_JSON, profileAsset,
+            asset->RTTI_GetType(), m_serializeContext);
+    }
+    /// OpenXRInteractionProfilesAssetHandler
+    ///////////////////////////////////////////////////////////
+
+} // namespace OpenXRVk

+ 12 - 0
Gems/OpenXRVk/Code/Source/OpenXRVkSystemComponent.cpp

@@ -38,6 +38,8 @@ namespace OpenXRVk
         }
 
         AzFramework::InputDeviceXRController::Reflect(context);
+        OpenXRInteractionProfilesAsset::Reflect(context);
+        OpenXRActionSetsAsset::Reflect(context);
     }
 
     XR::Ptr<XR::Instance> SystemComponent::CreateInstance()
@@ -82,6 +84,12 @@ namespace OpenXRVk
 
     void SystemComponent::Activate()
     {
+        m_actionSetsAssetHandler = AZStd::make_unique<OpenXRActionSetsAssetHandler>();
+        m_actionSetsAssetHandler->Register();
+
+        m_interactionProfilesAssetHandler = AZStd::make_unique<OpenXRInteractionProfilesAssetHandler>();
+        m_interactionProfilesAssetHandler->Register();
+
         if (XR::IsOpenXREnabled())
         {
             m_instance = AZStd::static_pointer_cast<OpenXRVk::Instance>(CreateInstance());
@@ -110,5 +118,9 @@ namespace OpenXRVk
             AZ::Interface<XR::Instance>::Unregister(m_instance.get());
             m_instance = nullptr;
         }
+
+        m_actionSetsAssetHandler->Unregister();
+        m_interactionProfilesAssetHandler->Unregister();
     }
+
 }

+ 14 - 0
Gems/OpenXRVk/Code/openxrvk_private_builder_files.cmake

@@ -0,0 +1,14 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(FILES
+    Source/Builders/OpenXRVkAssetsBuilder.cpp
+    Source/Builders/OpenXRVkAssetsBuilder.h
+    Source/Builders/OpenXRVkAssetsBuilderSystemComponent.cpp
+    Source/Builders/OpenXRVkAssetsBuilderSystemComponent.h
+)

+ 6 - 0
Gems/OpenXRVk/Code/openxrvk_private_common_files.cmake

@@ -17,6 +17,9 @@ set(FILES
     Include/OpenXRVk/OpenXRVkSwapChain.h
     Include/OpenXRVk/OpenXRVkSystemComponent.h
     Include/OpenXRVk/OpenXRVkUtils.h
+    Include/OpenXRVk/OpenXRVkInteractionProfilesAsset.h
+    Include/OpenXRVk/OpenXRVkActionSetsAsset.h
+    Include/OpenXRVk/OpenXRVkAssetsValidator.h
     Source/InputDeviceXRController.cpp
     Source/OpenXRVkCommon.h
     Source/OpenXRVkDevice.cpp
@@ -30,4 +33,7 @@ set(FILES
     Source/OpenXRVkUtils.cpp
     Source/XRCameraMovementComponent.cpp
     Source/XRCameraMovementComponent.h
+    Source/OpenXRVkInteractionProfilesAsset.cpp
+    Source/OpenXRVkActionSetsAsset.cpp
+    Source/OpenXRVkAssetsValidator.cpp
 )

+ 11 - 0
Gems/OpenXRVk/Code/openxrvk_shared_builder_files.cmake

@@ -0,0 +1,11 @@
+#
+# Copyright (c) Contributors to the Open 3D Engine Project.
+# For complete copyright and license terms please see the LICENSE at the root of this distribution.
+#
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+#
+
+set(FILES
+    Source/Builders/OpenXRVkBuilderModule.cpp
+)