#include "plugin.h"
static plugfsfuncs_t *fsfuncs;
static pluginputfuncs_t *inputfuncs;

#include "../engine/client/vr.h"

//#define XR_NO_PROTOTYPES

//figure out which platforms(read: windowing apis) we need...
#if defined(_WIN32)
	#define XR_USE_PLATFORM_WIN32
//#elif defined(ANDROID)
//	#define XR_USE_PLATFORM_ANDROID
#else
	#ifndef NO_X11
		#define XR_USE_PLATFORM_XLIB
	#endif
	#if defined(GLQUAKE) && defined(USE_EGL)
		//wayland, android, and x11-egl can all just use the EGL path...
		//at least once the openxr spec gets fixed (the wayland extension is apparently basically unusable)
		//note: XR_MND_egl_enable is a vendor extension to work around openxr stupidly trying to ignore it entirely.
		#define XR_USE_PLATFORM_EGL
	#endif
#endif

//figure out which graphics apis we need...
#ifdef GLQUAKE
	#define XR_USE_GRAPHICS_API_OPENGL
#endif
#ifdef VKQUAKE
	#define XR_USE_GRAPHICS_API_VULKAN
#endif
#ifdef D3D11QUAKE
	#ifdef _WIN32
		#define XR_USE_GRAPHICS_API_D3D11
	#endif
#endif

//include any headers we need for things to make sense.
#ifdef XR_USE_GRAPHICS_API_OPENGL
	#include "glquake.h"
#endif
#ifdef XR_USE_GRAPHICS_API_VULKAN
	#include "../engine/vk/vkrenderer.h"
#endif
#ifdef XR_USE_GRAPHICS_API_D3D11
	#include <d3d11.h>
#endif
#ifdef XR_USE_PLATFORM_EGL
	#include "gl_videgl.h"
#endif
#ifdef XR_USE_PLATFORM_XLIB
	#include <GL/glx.h>
#endif

//and finally include openxr stuff now that its hopefully not going to fail about missing typedefs.
#include <openxr/openxr_platform.h>
#if XR_CURRENT_API_VERSION < XR_MAKE_VERSION(1, 0, 16)
#define XR_ERROR_RUNTIME_UNAVAILABLE -51	//available starting 1.0.16
#endif

#ifdef XR_NO_PROTOTYPES
#define XRFUNCS		\
		XRFUNC(xrGetInstanceProcAddr)	\
		XRFUNC(xrResultToString)	\
		XRFUNC(xrEnumerateApiLayerProperties) \
		XRFUNC(xrEnumerateInstanceExtensionProperties)	\
		XRFUNC(xrCreateInstance)	\
		XRFUNC(xrGetInstanceProperties)	\
		XRFUNC(xrGetSystem)	\
		XRFUNC(xrGetSystemProperties)	\
		XRFUNC(xrEnumerateViewConfigurations) \
		XRFUNC(xrEnumerateViewConfigurationViews)	\
		XRFUNC(xrCreateSession)	\
		XRFUNC(xrCreateReferenceSpace)	\
		XRFUNC(xrCreateActionSet)	\
		XRFUNC(xrStringToPath)	\
		XRFUNC(xrCreateAction)	\
		XRFUNC(xrSuggestInteractionProfileBindings)	\
		XRFUNC(xrCreateActionSpace)	\
		XRFUNC(xrAttachSessionActionSets)	\
		XRFUNC(xrSyncActions)	\
		XRFUNC(xrGetActionStatePose)	\
		XRFUNC(xrApplyHapticFeedback)	\
		XRFUNC(xrLocateSpace)	\
		XRFUNC(xrGetActionStateBoolean)	\
		XRFUNC(xrGetActionStateFloat)	\
		XRFUNC(xrGetActionStateVector2f)	\
		XRFUNC(xrGetCurrentInteractionProfile)	\
		XRFUNC(xrEnumerateBoundSourcesForAction)	\
		XRFUNC(xrGetInputSourceLocalizedName)	\
		XRFUNC(xrPathToString)	\
		XRFUNC(xrCreateSwapchain)	\
		XRFUNC(xrEnumerateSwapchainFormats)	\
		XRFUNC(xrEnumerateSwapchainImages)	\
		XRFUNC(xrPollEvent)	\
		XRFUNC(xrBeginSession)	\
		XRFUNC(xrWaitFrame)	\
		XRFUNC(xrBeginFrame)	\
		XRFUNC(xrLocateViews)	\
		XRFUNC(xrAcquireSwapchainImage)	\
		XRFUNC(xrWaitSwapchainImage)	\
		XRFUNC(xrReleaseSwapchainImage)	\
		XRFUNC(xrEndFrame)	\
		XRFUNC(xrRequestExitSession)	\
		XRFUNC(xrEndSession)	\
		XRFUNC(xrDestroySwapchain)	\
		XRFUNC(xrDestroySpace)	\
		XRFUNC(xrDestroySession)	\
		XRFUNC(xrDestroyInstance)
#define XRFUNC(n) static PFN_##n n;
XRFUNCS
#undef XRFUNC
#endif

#ifdef XR_EXT_hand_tracking
static PFN_xrCreateHandTrackerEXT	xrCreateHandTrackerEXT;
static PFN_xrLocateHandJointsEXT	xrLocateHandJointsEXT;
static PFN_xrDestroyHandTrackerEXT	xrDestroyHandTrackerEXT;
#endif


#ifdef SVNREVISION
	#define APPLICATIONVERSION atoi(STRINGIFY(SVNREVISION))
	#define ENGINEVERSION atoi(STRINGIFY(SVNREVISION))
#else
	#define APPLICATIONVERSION 0
	#define ENGINEVERSION 0
#endif

static cvar_t *xr_enable;
static cvar_t *xr_debug;
static cvar_t *xr_formfactor;
static cvar_t *xr_viewconfig;
static cvar_t *xr_metresize;
static cvar_t *xr_skipregularview;
static cvar_t *xr_fingertracking;

static void XR_SetupInputs_Instance(void);

#define METRES_TO_QUAKE(x) ((x)*xr_metresize->value)
#define QUAKE_TO_METRES(x) ((x)/xr_metresize->value)

#define SECONDS_TO_NANOSECONDS(x) ((x)*1000000000)
#define NANOSECONDS_TO_SECONDS(x) ((x)/1000000000.0)

static void XR_PoseToAngOrg(const XrPosef *pose, vec3_t ang, vec3_t org)
{
	XrQuaternionf q = pose->orientation;
    const float sqw = q.w * q.w;
    const float sqx = q.x * q.x;
    const float sqy = q.y * q.y;
    const float sqz = q.z * q.z;

    ang[PITCH] = -asin(-2 * (q.y * q.z - q.w * q.x)) * (180/M_PI);
    ang[YAW] = atan2(2 * (q.x * q.z + q.w * q.y), sqw - sqx - sqy + sqz) * (180/M_PI);
    ang[ROLL] = -atan2(2 * (q.x * q.y + q.w * q.z), sqw - sqx + sqy - sqz) * (180/M_PI);

#if 1
	org[0]  =     METRES_TO_QUAKE(-pose->position.z);
	org[1]  =     METRES_TO_QUAKE(-pose->position.x);
	org[2]  =     METRES_TO_QUAKE(pose->position.y);
#else
	org[0]  =     METRES_TO_QUAKE(pose->position.x);
	org[1]  =     METRES_TO_QUAKE(pose->position.y);
	org[2]  =     METRES_TO_QUAKE(pose->position.z);
#endif
}

enum actset_e
{
	AS_COMMON,
	AS_MENU,
	AS_GAME,
	MAX_ACTIONSETS
};

static struct
{
//instance state (in case we want to start up)
	XrInstance instance;	//loader context
	XrSystemId systemid;	//device type thingie we're going for
#define MAX_VIEW_COUNT 12	//kinda abusive, but that's VR for you.
	unsigned int viewcount;
	XrViewConfigurationView *views;
	XrViewConfigurationType viewtype;

//engine context info (for restarting sessions)
	int renderer;			//rendering api we're using
	void *bindinginfo;		//appropriate XrGraphicsBinding*KHR struct so we can restart sessions.

//session state
	XrSession session;		//driver context
	XrSessionState state;
	qboolean beginning;
	qboolean ending;
	XrSpace space;
	struct
	{	//basically just swapchain state.
		XrSwapchain swapchain;
		unsigned int numswapimages;
		XrSwapchainSubImage subimage;
		image_t *swapimages;
	} eye[MAX_VIEW_COUNT];	//note that eye is a vauge term.

	XrActiveActionSet actionset[MAX_ACTIONSETS];

	qboolean timeknown;
	XrTime time;
	XrFrameState framestate;
	qboolean needrender;	//we MUST call xrBegin before the next xrWait
	int srgb;	//<0 = gamma-only. 0 = no srgb at all, >0 full srgb, including textures and stuff
	int colourformat;

	unsigned int numactions;
	unsigned int maxactions;
	struct
	{
		enum actset_e set;
		XrActionType acttype;
		const char *actname;		//doubles up as command names for buttons
		const char *actdescription;	//user-visible string (exposed via openxr runtime somehow)
		const char *subactionpath;	//somethingblahblah

		XrAction	action;	//for querying.
		XrPath		path;	//for querying.
		XrSpace		space;	//for poses.
		qboolean	held;	//for buttons.
	} *actions;
	qboolean inputsdirty;	//mostly for printing them.

#ifdef XR_EXT_hand_tracking
	struct
	{
		XrHandTrackerEXT handle;
		qboolean active;
		XrHandJointLocationEXT jointloc[XR_HAND_JOINT_COUNT_EXT];
		XrHandJointVelocityEXT jointvel[XR_HAND_JOINT_COUNT_EXT];
	} hand[2];
#endif
} xr;

static qboolean QDECL XR_PluginMayUnload(void)
{
	if (xr.instance)
		return false;	//something is still using us... don't let our code go away.
	return true;
}
static void XR_SessionEnded(void)
{
	size_t u;
	if (xr.space)
	{
		xrDestroySpace(xr.space);
		xr.space = XR_NULL_HANDLE;
	}

	for (u = 0; u < countof(xr.eye); u++)
	{
		free(xr.eye[u].swapimages);
		xr.eye[u].swapimages = NULL;
		xr.eye[u].numswapimages = 0;
		if (xr.eye[u].swapchain)
		{
			xrDestroySwapchain(xr.eye[u].swapchain);
			xr.eye[u].swapchain = XR_NULL_HANDLE;
		}
	}

#ifdef XR_EXT_hand_tracking
	for (u = 0; u < countof(xr.hand); u++)
	{
		if (xr.hand[u].handle)
			xrDestroyHandTrackerEXT(xr.hand[u].handle);
		xr.hand[u].handle = XR_NULL_HANDLE;
		xr.hand[u].active = false;
	}
#endif

	if (xr.session)
	{
		xrDestroySession(xr.session);
		xr.session = XR_NULL_HANDLE;
	}
	xr.state = XR_SESSION_STATE_UNKNOWN;
	xr.beginning = false;
}
static void XR_Shutdown(void)
{	//called on any kind of failure
	XR_SessionEnded();

	free(xr.bindinginfo);
	free(xr.views);
	if (xr.instance)
		xrDestroyInstance(xr.instance);

	memset(&xr, 0, sizeof(xr));
}

static const char *XR_StringForResult(XrResult res)
{
#if 0
	//this is a bit of a joke really. xrResultToString requires a valid instance so is unusable for printing out the various reasons why we might fail to create an instance.
	static char buffer[XR_MAX_RESULT_STRING_SIZE];
	if (XR_SUCCEEDED(res=xrResultToString(xr.instance, res, buffer)))
		return buffer;
	return va("XrResult %i", res);
#else
	switch(res)
	{
	case XR_SUCCESS: return "XR_SUCCESS";
	case XR_TIMEOUT_EXPIRED: return "XR_TIMEOUT_EXPIRED";
	case XR_SESSION_LOSS_PENDING: return "XR_SESSION_LOSS_PENDING";
	case XR_EVENT_UNAVAILABLE: return "XR_EVENT_UNAVAILABLE";
	case XR_SPACE_BOUNDS_UNAVAILABLE: return "XR_SPACE_BOUNDS_UNAVAILABLE";
	case XR_SESSION_NOT_FOCUSED: return "XR_SESSION_NOT_FOCUSED";
	case XR_FRAME_DISCARDED: return "XR_FRAME_DISCARDED";
	case XR_ERROR_VALIDATION_FAILURE: return "XR_ERROR_VALIDATION_FAILURE";
	case XR_ERROR_RUNTIME_FAILURE: return "XR_ERROR_RUNTIME_FAILURE";
	case XR_ERROR_OUT_OF_MEMORY: return "XR_ERROR_OUT_OF_MEMORY";
	case XR_ERROR_API_VERSION_UNSUPPORTED: return "XR_ERROR_API_VERSION_UNSUPPORTED";
	case XR_ERROR_INITIALIZATION_FAILED: return "XR_ERROR_INITIALIZATION_FAILED";
	case XR_ERROR_FUNCTION_UNSUPPORTED: return "XR_ERROR_FUNCTION_UNSUPPORTED";
	case XR_ERROR_FEATURE_UNSUPPORTED: return "XR_ERROR_FEATURE_UNSUPPORTED";
	case XR_ERROR_EXTENSION_NOT_PRESENT: return "XR_ERROR_EXTENSION_NOT_PRESENT";
	case XR_ERROR_LIMIT_REACHED: return "XR_ERROR_LIMIT_REACHED";
	case XR_ERROR_SIZE_INSUFFICIENT: return "XR_ERROR_SIZE_INSUFFICIENT";
	case XR_ERROR_HANDLE_INVALID: return "XR_ERROR_HANDLE_INVALID";
	case XR_ERROR_INSTANCE_LOST: return "XR_ERROR_INSTANCE_LOST";
	case XR_ERROR_SESSION_RUNNING: return "XR_ERROR_SESSION_RUNNING";
	case XR_ERROR_SESSION_NOT_RUNNING: return "XR_ERROR_SESSION_NOT_RUNNING";
	case XR_ERROR_SESSION_LOST: return "XR_ERROR_SESSION_LOST";
	case XR_ERROR_SYSTEM_INVALID: return "XR_ERROR_SYSTEM_INVALID";
	case XR_ERROR_PATH_INVALID: return "XR_ERROR_PATH_INVALID";
	case XR_ERROR_PATH_COUNT_EXCEEDED: return "XR_ERROR_PATH_COUNT_EXCEEDED";
	case XR_ERROR_PATH_FORMAT_INVALID: return "XR_ERROR_PATH_FORMAT_INVALID";
	case XR_ERROR_PATH_UNSUPPORTED: return "XR_ERROR_PATH_UNSUPPORTED";
	case XR_ERROR_LAYER_INVALID: return "XR_ERROR_LAYER_INVALID";
	case XR_ERROR_LAYER_LIMIT_EXCEEDED: return "XR_ERROR_LAYER_LIMIT_EXCEEDED";
	case XR_ERROR_SWAPCHAIN_RECT_INVALID: return "XR_ERROR_SWAPCHAIN_RECT_INVALID";
	case XR_ERROR_SWAPCHAIN_FORMAT_UNSUPPORTED: return "XR_ERROR_SWAPCHAIN_FORMAT_UNSUPPORTED";
	case XR_ERROR_ACTION_TYPE_MISMATCH: return "XR_ERROR_ACTION_TYPE_MISMATCH";
	case XR_ERROR_SESSION_NOT_READY: return "XR_ERROR_SESSION_NOT_READY";
	case XR_ERROR_SESSION_NOT_STOPPING: return "XR_ERROR_SESSION_NOT_STOPPING";
	case XR_ERROR_TIME_INVALID: return "XR_ERROR_TIME_INVALID";
	case XR_ERROR_REFERENCE_SPACE_UNSUPPORTED: return "XR_ERROR_REFERENCE_SPACE_UNSUPPORTED";
	case XR_ERROR_FILE_ACCESS_ERROR: return "XR_ERROR_FILE_ACCESS_ERROR";
	case XR_ERROR_FILE_CONTENTS_INVALID: return "XR_ERROR_FILE_CONTENTS_INVALID";
	case XR_ERROR_FORM_FACTOR_UNSUPPORTED: return "XR_ERROR_FORM_FACTOR_UNSUPPORTED";
	case XR_ERROR_FORM_FACTOR_UNAVAILABLE: return "XR_ERROR_FORM_FACTOR_UNAVAILABLE";
	case XR_ERROR_API_LAYER_NOT_PRESENT: return "XR_ERROR_API_LAYER_NOT_PRESENT";
	case XR_ERROR_CALL_ORDER_INVALID: return "XR_ERROR_CALL_ORDER_INVALID";
	case XR_ERROR_GRAPHICS_DEVICE_INVALID: return "XR_ERROR_GRAPHICS_DEVICE_INVALID";
	case XR_ERROR_POSE_INVALID: return "XR_ERROR_POSE_INVALID";
	case XR_ERROR_INDEX_OUT_OF_RANGE: return "XR_ERROR_INDEX_OUT_OF_RANGE";
	case XR_ERROR_VIEW_CONFIGURATION_TYPE_UNSUPPORTED: return "XR_ERROR_VIEW_CONFIGURATION_TYPE_UNSUPPORTED";
	case XR_ERROR_ENVIRONMENT_BLEND_MODE_UNSUPPORTED: return "XR_ERROR_ENVIRONMENT_BLEND_MODE_UNSUPPORTED";
	case XR_ERROR_NAME_DUPLICATED: return "XR_ERROR_NAME_DUPLICATED";
	case XR_ERROR_NAME_INVALID: return "XR_ERROR_NAME_INVALID";
	case XR_ERROR_ACTIONSET_NOT_ATTACHED: return "XR_ERROR_ACTIONSET_NOT_ATTACHED";
	case XR_ERROR_ACTIONSETS_ALREADY_ATTACHED: return "XR_ERROR_ACTIONSETS_ALREADY_ATTACHED";
	case XR_ERROR_LOCALIZED_NAME_DUPLICATED: return "XR_ERROR_LOCALIZED_NAME_DUPLICATED";
	case XR_ERROR_LOCALIZED_NAME_INVALID: return "XR_ERROR_LOCALIZED_NAME_INVALID";
//	case XR_ERROR_RUNTIME_UNAVAILABLE: return "XR_ERROR_RUNTIME_UNAVAILABLE";
	default:
		return va("XrResult %i", res);
	}
#endif
}


static XrBool32 XRAPI_CALL XR_DebugPrint(XrDebugUtilsMessageSeverityFlagsEXT messageSeverity, XrDebugUtilsMessageTypeFlagsEXT messageTypes, const XrDebugUtilsMessengerCallbackDataEXT *callbackData, void *userData)
{
	char *sev;
	switch(messageSeverity)
	{
	case 1/*XR_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT*/:		sev = "^8";			break;
	case 16/*XR_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT*/:		sev = "";			break;
	case 256/*XR_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT*/:	sev = CON_WARNING;	break;
	default:
	case 4096/*XR_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT*/:		sev = CON_ERROR;	break;
	}

	Con_Printf("%s%s: %s\n", sev, callbackData->functionName, callbackData->message);
	return XR_FALSE;
}

static qboolean XR_PreInit(vrsetup_t *qreqs)
{
	XrResult res;
	const char *xrexts[8], *ext;
	const char *xrlayers[8];
	uint32_t numext = 0, numlayers = 0;
	qboolean havedebugutils = false;
#ifdef XR_EXT_hand_tracking
	qboolean havehandtrack = false;
#endif

	XR_Shutdown();	//just in case...

	if (qreqs->structsize != sizeof(*qreqs) || xr_enable->ival < 0)
		return false;	//nope, get lost.
	if (!strncasecmp(xr_formfactor->string, "none", 4))
		qreqs->vrplatform = VR_HEADLESS;
	switch(qreqs->vrplatform)
	{
#ifdef XR_MND_HEADLESS_EXTENSION_NAME
	case VR_HEADLESS:
		ext = XR_MND_HEADLESS_EXTENSION_NAME;
		break;
#endif
#ifdef XR_USE_GRAPHICS_API_VULKAN
	case VR_VULKAN:
		ext = XR_KHR_VULKAN_ENABLE_EXTENSION_NAME;
		break;
#endif
#ifdef XR_USE_GRAPHICS_API_OPENGL
#ifdef XR_MND_EGL_ENABLE_EXTENSION_NAME
	case VR_EGL:
		ext = XR_MND_EGL_ENABLE_EXTENSION_NAME;
		break;
#elif defined(XR_MNDX_EGL_ENABLE_EXTENSION_NAME)
	case VR_EGL:
		ext = XR_MNDX_EGL_ENABLE_EXTENSION_NAME;
		break;
#endif
#ifdef XR_USE_PLATFORM_XLIB
	case VR_X11_GLX:
#endif
#ifdef XR_USE_PLATFORM_WIN32
	case VR_WIN_WGL:
#endif
		ext = XR_KHR_OPENGL_ENABLE_EXTENSION_NAME;
		break;
#endif
#ifdef XR_USE_GRAPHICS_API_D3D11
	case VR_D3D11:
		ext = XR_KHR_D3D11_ENABLE_EXTENSION_NAME;
		break;
#endif
	default:
		Con_Printf(CON_ERROR"OpenXR: windowing-api or rendering-api not supported\n");
		return false;
	}

	xr.instance = XR_NULL_HANDLE;

	if (xr_debug->ival)
	{
		uint32_t count, u;
		struct XrApiLayerProperties *props;
		if (XR_SUCCEEDED(xrEnumerateApiLayerProperties(0, &count, NULL)))
		{
			props = calloc(count, sizeof(*props));
			for (u = 0; u < count; u++)
				props[u].type = XR_TYPE_API_LAYER_PROPERTIES;
			xrEnumerateApiLayerProperties(count, &count, props);
			for (u = 0; u < count; u++)
				Con_Printf("OpenXR Layer %s: %s\n", props[u].layerName, props[u].description);

			if (xr_debug->ival>1)
			{
				for (u = 0; u < count; u++)
					if (!strcmp(props[u].layerName, "XR_APILAYER_LUNARG_core_validation"))
					{
						xrlayers[numlayers++] = "XR_APILAYER_LUNARG_core_validation";
						break;
					}
				if (u==count)
					Con_Printf("OpenXR: Validation layers not found\n");
			}
			free(props);
		}
	}

	{
		unsigned int exts = 0, u=0;
		XrExtensionProperties *extlist;
		res = xrEnumerateInstanceExtensionProperties(NULL, 0, &exts, NULL);
		if (res == XR_ERROR_RUNTIME_UNAVAILABLE || res == XR_ERROR_INSTANCE_LOST)
		{
			Con_Printf(CON_WARNING"OpenXR: no runtime installed\n");
			return false;
		}
		else if (XR_SUCCEEDED(res))
		{
			extlist = calloc(exts, sizeof(*extlist));
			for (u = 0; u < exts; u++)
				extlist[u].type = XR_TYPE_EXTENSION_PROPERTIES;
			xrEnumerateInstanceExtensionProperties(NULL, exts, &exts, extlist);

			//print a list of them all, if we can.
			if (xr_debug->ival)
			{
				Con_DPrintf("OpenXR:\n");
				for (u = 0; u < exts; u++)
					Con_DPrintf("\t%s\n", extlist[u].extensionName);
			}

			//make sure we have an appropriate extension for the API we're using.
			if (ext)
			{
				for (u = 0; u < exts; u++)
					if (!strcmp(extlist[u].extensionName, ext))
						break;
				if (u == exts)
				{
					Con_Printf(CON_ERROR"OpenXR: instance driver does not support required %s\n", ext);
					free(extlist);
					return false;	//would just give an error on xrCreateInstance anyway.
				}

				xrexts[numext++] = ext;
			}

			//look for some interesting extensions
			for (u = 0; u < exts; u++)
			{
				if (!strcmp(extlist[u].extensionName, XR_EXT_DEBUG_UTILS_EXTENSION_NAME) && !havedebugutils && xr_debug->ival)
					havedebugutils = true, xrexts[numext++] = XR_EXT_DEBUG_UTILS_EXTENSION_NAME;

#ifdef XR_EXT_hand_tracking
				if (!strcmp(extlist[u].extensionName, XR_EXT_HAND_TRACKING_EXTENSION_NAME) && !havehandtrack && xr_fingertracking->ival)
					havehandtrack = true, xrexts[numext++] = XR_EXT_HAND_TRACKING_EXTENSION_NAME;
#endif
			}

			free(extlist);
		}
		else
		{
			Con_Printf(CON_ERROR"OpenXR: xrEnumerateInstanceExtensionProperties failed (%s)\n", XR_StringForResult(res));
			return false;
		}
	}

	//create our instance
	{
		XrInstanceCreateInfo createinfo = {XR_TYPE_INSTANCE_CREATE_INFO};
		createinfo.createFlags = 0;
		Q_strlcpy(createinfo.applicationInfo.applicationName, FULLENGINENAME, sizeof(createinfo.applicationInfo.applicationName));
		createinfo.applicationInfo.applicationVersion = APPLICATIONVERSION;
		Q_strlcpy(createinfo.applicationInfo.engineName, "FTEQW", sizeof(createinfo.applicationInfo.engineName));
		createinfo.applicationInfo.engineVersion = ENGINEVERSION;
		createinfo.applicationInfo.apiVersion = XR_CURRENT_API_VERSION;
		createinfo.enabledApiLayerCount = numlayers;
		createinfo.enabledApiLayerNames = xrlayers;
		createinfo.enabledExtensionCount = numext;
		createinfo.enabledExtensionNames = xrexts;
		res = xrCreateInstance(&createinfo, &xr.instance);
	}
	if (res == XR_ERROR_RUNTIME_UNAVAILABLE || res == XR_ERROR_INSTANCE_LOST)
	{
		Con_Printf(CON_WARNING"OpenXR: no runtime installed\n");
		return false;
	}
	if (XR_FAILED(res) || !xr.instance)
	{
		Con_Printf(CON_ERROR"OpenXR Runtime: xrCreateInstance failed (%s)\n", XR_StringForResult(res));
		return false;
	}

	if (havedebugutils)
	{
		XrDebugUtilsMessengerEXT messenger1 = XR_NULL_HANDLE;
		XrDebugUtilsMessengerCreateInfoEXT cb = {XR_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT};
		PFN_xrCreateDebugUtilsMessengerEXT xrCreateDebugUtilsMessengerEXT;
		cb.messageSeverities = XR_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT | XR_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | XR_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT | XR_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT;
		cb.messageTypes = XR_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |  XR_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | XR_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT | XR_DEBUG_UTILS_MESSAGE_TYPE_CONFORMANCE_BIT_EXT;
		cb.userCallback = XR_DebugPrint;
		cb.userData = NULL;

		if (!XR_FAILED(xrGetInstanceProcAddr(xr.instance, "xrCreateDebugUtilsMessengerEXT", (PFN_xrVoidFunction*) &xrCreateDebugUtilsMessengerEXT)))
			xrCreateDebugUtilsMessengerEXT(xr.instance, &cb, &messenger1);
	}

	if (xr_debug->ival)
	{
		XrInstanceProperties props = {XR_TYPE_INSTANCE_PROPERTIES};
		if (!XR_FAILED(xrGetInstanceProperties(xr.instance, &props)))
			Con_Printf("OpenXR Runtime: %s    %u.%u.%u\n", props.runtimeName, XR_VERSION_MAJOR(props.runtimeVersion), XR_VERSION_MINOR(props.runtimeVersion), XR_VERSION_PATCH(props.runtimeVersion));
		else
			Con_Printf("OpenXR Runtime: Unable to determine runtime version (%s)\n", XR_StringForResult(res));
	}

	{
		XrSystemGetInfo systemInfo = { XR_TYPE_SYSTEM_GET_INFO };
		if (qreqs->vrplatform == VR_HEADLESS)
			systemInfo.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;	//err... woteva
		else if (!strncasecmp(xr_formfactor->string, "hand", 4))
			systemInfo.formFactor = XR_FORM_FACTOR_HANDHELD_DISPLAY;
		else if (!strncasecmp(xr_formfactor->string, "head",4))
			systemInfo.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
		else
		{
			if (*xr_formfactor->string)
				Con_Printf("\"%s\" is not a recognised value for xr_formfactor\n", xr_formfactor->string);
			else
				Con_Printf("xr_formfactor not set, assuming headmounted\n");
			systemInfo.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
		}
		res = xrGetSystem(xr.instance, &systemInfo, &xr.systemid);
		if (XR_FAILED(res) || !xr.systemid)
			return false;
	}

	{
		XrSystemProperties props = {XR_TYPE_SYSTEM_PROPERTIES};
#ifdef XR_EXT_hand_tracking
		XrSystemHandTrackingPropertiesEXT handtrackprops = {XR_TYPE_SYSTEM_HAND_TRACKING_PROPERTIES_EXT};
		if (havehandtrack)
		{	//instance might support it, but the specific hardware we're trying to use might not, in which case don't try using it after all.
			handtrackprops.next = props.next;
			props.next = &handtrackprops;
		}
#endif
		if (qreqs->vrplatform != VR_HEADLESS)
			if (XR_SUCCEEDED(xrGetSystemProperties(xr.instance, xr.systemid, &props)))
			{
				if (xr_debug->ival)
					Con_Printf("OpenXR System: %s\n", props.systemName);
			}

#ifdef XR_EXT_hand_tracking
		havehandtrack = handtrackprops.supportsHandTracking;
#endif
	}

#ifdef XR_EXT_hand_tracking
	if (havehandtrack)
	{
		xrGetInstanceProcAddr(xr.instance, "xrCreateHandTrackerEXT", (PFN_xrVoidFunction*) &xrCreateHandTrackerEXT);
		xrGetInstanceProcAddr(xr.instance, "xrLocateHandJointsEXT", (PFN_xrVoidFunction*) &xrLocateHandJointsEXT);
		xrGetInstanceProcAddr(xr.instance, "xrDestroyHandTrackerEXT", (PFN_xrVoidFunction*) &xrDestroyHandTrackerEXT);
	}
	else
	{
		xrCreateHandTrackerEXT = NULL;
		xrLocateHandJointsEXT = NULL;
		xrDestroyHandTrackerEXT = NULL;
	}
#endif

	switch(qreqs->vrplatform)
	{
	default:
		XR_Shutdown();
		return false;
	case VR_HEADLESS:
		break;

#ifdef XR_USE_GRAPHICS_API_VULKAN
	case VR_VULKAN:
		{
			XrGraphicsRequirementsVulkanKHR reqs = {XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR};
			VkInstance inst = VK_NULL_HANDLE;
			VkPhysicalDevice physdev;
			uint32_t extlen;
			char *extstr;	//space-delimited list, for some reason. writable though.

			PFN_xrGetVulkanGraphicsRequirementsKHR xrGetVulkanGraphicsRequirementsKHR;
			PFN_xrGetVulkanInstanceExtensionsKHR xrGetVulkanInstanceExtensionsKHR;
			PFN_xrGetVulkanDeviceExtensionsKHR xrGetVulkanDeviceExtensionsKHR;
			PFN_xrGetVulkanGraphicsDeviceKHR xrGetVulkanGraphicsDeviceKHR;
			if (XR_FAILED(xrGetInstanceProcAddr(xr.instance, "xrGetVulkanGraphicsRequirementsKHR", (PFN_xrVoidFunction*)&xrGetVulkanGraphicsRequirementsKHR)) ||
				XR_FAILED(xrGetInstanceProcAddr(xr.instance, "xrGetVulkanInstanceExtensionsKHR", (PFN_xrVoidFunction*)&xrGetVulkanInstanceExtensionsKHR)) ||
				XR_FAILED(xrGetInstanceProcAddr(xr.instance, "xrGetVulkanDeviceExtensionsKHR", (PFN_xrVoidFunction*)&xrGetVulkanDeviceExtensionsKHR)) ||
				XR_FAILED(xrGetInstanceProcAddr(xr.instance, "xrGetVulkanGraphicsDeviceKHR", (PFN_xrVoidFunction*)&xrGetVulkanGraphicsDeviceKHR)))
				return false;
			xrGetVulkanGraphicsRequirementsKHR(xr.instance, xr.systemid, &reqs);
			qreqs->maxver.major = XR_VERSION_MAJOR(reqs.maxApiVersionSupported);
			qreqs->maxver.minor = XR_VERSION_MINOR(reqs.maxApiVersionSupported);
			qreqs->minver.major = XR_VERSION_MAJOR(reqs.minApiVersionSupported);
			qreqs->minver.minor = XR_VERSION_MINOR(reqs.minApiVersionSupported);

			xrGetVulkanInstanceExtensionsKHR(xr.instance, xr.systemid, 0, &extlen, NULL);
			extstr = malloc(extlen);
			xrGetVulkanInstanceExtensionsKHR(xr.instance, xr.systemid, extlen, &extlen, extstr);

			//create vulkan instance now...
			qreqs->createinstance(qreqs, extstr, &inst);
			free(extstr);

			xrGetVulkanDeviceExtensionsKHR(xr.instance, xr.systemid, 0, &extlen, NULL);
			extstr = malloc(extlen);
			xrGetVulkanDeviceExtensionsKHR(xr.instance, xr.systemid, extlen, &extlen, extstr);

			res = xrGetVulkanGraphicsDeviceKHR(xr.instance, xr.systemid, inst, &physdev);

			qreqs->deviceextensions = extstr;
			qreqs->vk.physicaldevice = physdev;
		}
		break;
#endif

#ifdef XR_USE_GRAPHICS_API_OPENGL
	case VR_X11_GLX:
	case VR_EGL:
	case VR_WIN_WGL:
		{
			XrGraphicsRequirementsOpenGLKHR reqs = {XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR};
			PFN_xrGetOpenGLGraphicsRequirementsKHR xrGetOpenGLGraphicsRequirementsKHR;
			if (XR_SUCCEEDED(xrGetInstanceProcAddr(xr.instance, "xrGetOpenGLGraphicsRequirementsKHR", (PFN_xrVoidFunction*)&xrGetOpenGLGraphicsRequirementsKHR)))
				xrGetOpenGLGraphicsRequirementsKHR(xr.instance, xr.systemid, &reqs);

			qreqs->maxver.major = XR_VERSION_MAJOR(reqs.maxApiVersionSupported);
			qreqs->maxver.minor = XR_VERSION_MINOR(reqs.maxApiVersionSupported);
			qreqs->minver.major = XR_VERSION_MAJOR(reqs.minApiVersionSupported);
			qreqs->minver.minor = XR_VERSION_MINOR(reqs.minApiVersionSupported);
			//caller must validate when creating its context.
		}
		break;
#endif

#ifdef XR_USE_GRAPHICS_API_D3D11
	case VR_D3D11:
		{
			XrGraphicsRequirementsD3D11KHR reqs = {XR_TYPE_GRAPHICS_REQUIREMENTS_D3D11_KHR};
			PFN_xrGetD3D11GraphicsRequirementsKHR xrGetD3D11GraphicsRequirementsKHR;
			if (XR_SUCCEEDED(xrGetInstanceProcAddr(xr.instance, "xrGetD3D11GraphicsRequirementsKHR", (PFN_xrVoidFunction*)&xrGetD3D11GraphicsRequirementsKHR)))
				xrGetD3D11GraphicsRequirementsKHR(xr.instance, xr.systemid, &reqs);

			qreqs->minver.major = reqs.minFeatureLevel;
			qreqs->deviceid[0] = reqs.adapterLuid.LowPart;
			qreqs->deviceid[1] = reqs.adapterLuid.HighPart;
		}
		break;
#endif
	}

	{
		XrViewConfigurationType *viewtype;
		uint32_t viewtypes, u;
		res = xrEnumerateViewConfigurations(xr.instance, xr.systemid, 0, &viewtypes, NULL);
		viewtype = alloca(viewtypes*sizeof(viewtype));
		res = xrEnumerateViewConfigurations(xr.instance, xr.systemid, viewtypes, &viewtypes, viewtype);
		xr.viewtype = (XrViewConfigurationType)0;
		for (u = 0; u < viewtypes; u++)
		{
			switch(viewtype[u])
			{
			case XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MONO:
				if (!strcasecmp(xr_viewconfig->string, "mono"))
					xr.viewtype = viewtype[u];
				break;
			case XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO:
				if (!strcasecmp(xr_viewconfig->string, "stereo"))
					xr.viewtype = viewtype[u];
				break;
			case XR_VIEW_CONFIGURATION_TYPE_PRIMARY_QUAD_VARJO:
				if (!strcasecmp(xr_viewconfig->string, "quad"))
					xr.viewtype = viewtype[u];
				break;
			default:
				break;
			}
		}
		if (!xr.viewtype)
		{
			if (viewtypes)
				xr.viewtype = viewtype[0];

			if (*xr_viewconfig->string)
			{
				Con_Printf("OpenXR: Viewtype %s unavailable, using ", xr_viewconfig->string);
				switch(xr.viewtype)
				{
				case XR_VIEW_CONFIGURATION_TYPE_PRIMARY_MONO:		Con_Printf("mono\n"); break;
				case XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO:		Con_Printf("stereo\n"); break;
				case XR_VIEW_CONFIGURATION_TYPE_PRIMARY_QUAD_VARJO:	Con_Printf("quad\n"); break;
				default:
					Con_Printf("unknown (%i)\n", xr.viewtype);
					break;
				}
			}
		}
	}

	if (qreqs->vrplatform == VR_HEADLESS)
		return true;

	res = xrEnumerateViewConfigurationViews(xr.instance, xr.systemid, xr.viewtype, 0, &xr.viewcount, NULL);
	if (xr.viewcount > MAX_VIEW_COUNT)
		xr.viewcount = MAX_VIEW_COUNT;	//oh noes! evile!
	xr.views = calloc(1,sizeof(*xr.views)*xr.viewcount);
	{
		uint32_t u;
		for (u = 0; u < xr.viewcount; u++)
			xr.views[u].type = XR_TYPE_VIEW_CONFIGURATION_VIEW;
	}
	res = xrEnumerateViewConfigurationViews(xr.instance, xr.systemid, xr.viewtype, xr.viewcount, &xr.viewcount, xr.views);

	//caller now knows what device/contextversion/etc to init with
	return true;
}

static qboolean XR_Init(vrsetup_t *qreqs, rendererstate_t *info)
{
	xr.srgb = info->srgb;
	switch(qreqs->vrplatform)
	{
	case VR_HEADLESS:
		break;
	default:
		return false;	//error. not supported in this build.
#ifdef XR_USE_GRAPHICS_API_VULKAN
	case VR_VULKAN:
		{
			XrGraphicsBindingVulkanKHR *vk = xr.bindinginfo = calloc(1, sizeof(*vk));
			vk->type = XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR;
			vk->instance = qreqs->vk.instance;
			vk->physicalDevice = qreqs->vk.physicaldevice;
			vk->device = qreqs->vk.device;
			vk->queueFamilyIndex = qreqs->vk.queuefamily;
			vk->queueIndex = qreqs->vk.queueindex;
			xr.renderer = QR_VULKAN;
		}
		break;
#endif
#ifdef XR_USE_GRAPHICS_API_OPENGL
#ifdef XR_MND_EGL_ENABLE_EXTENSION_NAME
	case VR_EGL:	//x11-egl, wayland, and hopefully android...
		{
			XrGraphicsBindingEGLMND *egl = xr.bindinginfo = calloc(1, sizeof(*egl));
			egl->type = XR_TYPE_GRAPHICS_BINDING_EGL_MND;
			egl->getProcAddress = qreqs->egl.getprocaddr;
			egl->display = qreqs->egl.egldisplay;
			egl->config = qreqs->egl.eglconfig;
			egl->context = qreqs->egl.eglcontext;
			xr.renderer = QR_OPENGL;
		}
		break;
#elif defined(XR_MNDX_EGL_ENABLE_EXTENSION_NAME)
	case VR_EGL:	//x11-egl, wayland, and hopefully android...
		{
			XrGraphicsBindingEGLMNDX *egl = xr.bindinginfo = calloc(1, sizeof(*egl));
			egl->type = XR_TYPE_GRAPHICS_BINDING_EGL_MNDX;
			egl->getProcAddress = (PFNEGLGETPROCADDRESSPROC)qreqs->egl.getprocaddr;
			egl->display = qreqs->egl.egldisplay;
			egl->config = qreqs->egl.eglconfig;
			egl->context = qreqs->egl.eglcontext;
			xr.renderer = QR_OPENGL;
		}
		break;
#endif
#ifdef XR_USE_PLATFORM_XLIB
	case VR_X11_GLX:
		{
			XrGraphicsBindingOpenGLXlibKHR *glx = xr.bindinginfo = calloc(1, sizeof(*glx));
			glx->type = XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR;
			glx->xDisplay = qreqs->x11_glx.display;
			glx->visualid = qreqs->x11_glx.visualid;
			glx->glxFBConfig = qreqs->x11_glx.glxfbconfig;
			glx->glxDrawable = qreqs->x11_glx.drawable;
			glx->glxContext = qreqs->x11_glx.glxcontext;
			xr.renderer = QR_OPENGL;
		}
		break;
#endif
#ifdef XR_USE_PLATFORM_WIN32
	case VR_WIN_WGL:
		{
			XrGraphicsBindingOpenGLWin32KHR *wgl = xr.bindinginfo = calloc(1, sizeof(*wgl));
			wgl->type = XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR;
			wgl->hDC = qreqs->wgl.hdc;
			wgl->hGLRC = qreqs->wgl.hglrc;
			xr.renderer = QR_OPENGL;
		}
		break;
#endif
#endif	//def XR_USE_GRAPHICS_API_OPENGL
#ifdef XR_USE_GRAPHICS_API_D3D11
	case VR_D3D11:
		{
			XrGraphicsBindingD3D11KHR *d3d = xr.bindinginfo = calloc(1, sizeof(*d3d));
			d3d->type = XR_TYPE_GRAPHICS_BINDING_D3D11_KHR;
			d3d->device = qreqs->d3d.device;
			xr.renderer = QR_DIRECT3D11;
		}
		break;
#endif
	}


	XR_SetupInputs_Instance();
	return true;
}

static qboolean XR_HapticCommand_f(qboolean isinsecure)
{
	size_t u;
	char actionname[XR_MAX_ACTION_NAME_SIZE];
	cmdfuncs->Argv(0, actionname, sizeof(actionname));
	for (u = 0; u < xr.numactions; u++)
	{
		if (!strcasecmp(xr.actions[u].actname, actionname) && xr.actions[u].acttype == XR_ACTION_TYPE_VIBRATION_OUTPUT)
		{
			if (xr.session)
			{
				XrHapticActionInfo info = {XR_TYPE_HAPTIC_ACTION_INFO};
				XrHapticVibration haptic = {XR_TYPE_HAPTIC_VIBRATION};
				info.action = xr.actions[u].action;
				info.subactionPath = XR_NULL_PATH;

				cmdfuncs->Argv(1, actionname, sizeof(actionname));
				haptic.duration = *actionname?SECONDS_TO_NANOSECONDS(atof(actionname)):XR_MIN_HAPTIC_DURATION;
				cmdfuncs->Argv(2, actionname, sizeof(actionname));
				haptic.amplitude = *actionname?atof(actionname):0;
				cmdfuncs->Argv(3, actionname, sizeof(actionname));
				haptic.frequency = *actionname?atof(actionname):XR_FREQUENCY_UNSPECIFIED;
				xrApplyHapticFeedback(xr.session, &info, (XrHapticBaseHeader*)&haptic);
			}
			return true;
		}
	}
	return false;
}

static XrAction XR_DefineAction(enum actset_e set, XrActionType type, const char *name, const char *description, const char *root)
{
	XrActionCreateInfo info = {XR_TYPE_ACTION_CREATE_INFO};
	XrResult res;
	char *ffs;

	size_t u;
	int nconflicts = 0;
	int dconflicts = 0;
	for (u = 0; u < xr.numactions; u++)
	{
		if (xr.actions[u].set != set)
			continue;
		if ((xr.actions[u].acttype == type || !type) && !strcmp(xr.actions[u].actname, name) /*&& !strcmp(xr.actions[u].actdescription, description)*/ && !strcmp(xr.actions[u].subactionpath?xr.actions[u].subactionpath:"", root?root:""))
		{	//looks like a dupe...
			return xr.actions[u].action;
		}
		if (!strcasecmp(xr.actions[u].actname, name))
			nconflicts++;	//arse balls knob cock
		if (description && !strcasecmp(xr.actions[u].actdescription, description))
			dconflicts++;	//arse balls knob cock
	}
	if (!description)
		return XR_NULL_HANDLE;	//none found
	if (u == xr.maxactions)
	{
		size_t nm = (xr.maxactions+1)*2;
		void *n = plugfuncs->Realloc(xr.actions, sizeof(*xr.actions) * nm);
		if (!n)
			return XR_NULL_HANDLE;	//erk!
		xr.actions = n;
		xr.maxactions = nm;
	}

	memset(&xr.actions[u], 0, sizeof(xr.actions[u]));
	xr.actions[u].set = set;
	xr.actions[u].acttype = type;
	xr.actions[u].actname = strdup(name);
	xr.actions[u].actdescription = strdup(description);
	xr.actions[u].subactionpath = (root&&*root)?strdup(root):NULL;
	xr.numactions++;

	if (xr.actions[u].subactionpath)
		xrStringToPath(xr.instance, xr.actions[u].subactionpath, &xr.actions[u].path);
	else
		xr.actions[u].path = XR_NULL_PATH;
	if (xr.actions[u].path == XR_NULL_PATH)
	{
		info.countSubactionPaths = 0;
		info.subactionPaths = NULL;
	}
	else
	{
		info.countSubactionPaths = 1;
		info.subactionPaths = &xr.actions[u].path;
	}
	info.actionType = xr.actions[u].acttype;
	Q_strlcpy(info.actionName, xr.actions[u].actname, sizeof(info.actionName));
	for (ffs = info.actionName; *ffs; ffs++)
	{
		if (*ffs >= 'A' && *ffs < 'Z')
			*ffs += 'a'-'A';	//must be lower-case
		else if (*ffs >= 'a' && *ffs <= 'z')
			;	//allowed
		else if (*ffs >= '0' && *ffs <= '9')
			;	//allowed
		else if (*ffs == '.' || *ffs == '-' || *ffs == '_')
			;	//allowed
		// '/' is not allowed as it must be a single segment.
		else
			*ffs = '_';	//everything else is blocked
	}
	Q_strlcpy(info.localizedActionName, xr.actions[u].actdescription, sizeof(info.localizedActionName));
	res = xrCreateAction(xr.actionset[set].actionSet, &info, &xr.actions[u].action);
	if (XR_FAILED(res))
		Con_Printf("openxr: Unable to create action %s [%s] - %s\n", info.actionName, info.localizedActionName, XR_StringForResult(res));

	if (info.actionType == XR_ACTION_TYPE_VIBRATION_OUTPUT)
		cmdfuncs->AddCommand(xr.actions[u].actname);

	return xr.actions[u].action;
}

static qboolean XR_ReadLine(const char **text, char *buffer, size_t buflen)
{
	char in;
	char *out = buffer;
	size_t len;
	if (buflen <= 1)
		return false;
	len = buflen-1;
	while (len > 0)
	{
		in = *(*text);
		if (!in)
		{
			if (len == buflen-1)
				return false;
			*out = 0;
			return true;
		}
		(*text)++;
		if (in == '\n')
			break;
		*out++ = in;
		len--;
	}
	*out = '\0';

	//if there's a trailing \r, strip it.
	if (out > buffer)
		if (out[-1] == '\r')
			out[-1] = 0;

	return true;
}

static int XR_BindProfileStr(const char *fname, const char *file)
{
	XrAction act;
	XrResult res;
	XrPath profilepath = XR_NULL_PATH;
	char line[1024], *linestart;
	char name[1024];
	char type[256];
	char desc[1024];
	char bind[1024];
	char root[1024];
	unsigned int p;
	char prefix[2][1024];
	enum actset_e set;

	XrInteractionProfileSuggestedBinding suggestedbindings = {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
	unsigned int acts = 0;
	XrActionSuggestedBinding bindings[256];
	unsigned int totalacts = 0;

	while (XR_ReadLine(&file, line, sizeof(line)))
	{
		set = AS_COMMON;
		linestart = line;
		while (*linestart == ' ' || *linestart == '\t')
			linestart++;

		if (!strncasecmp(linestart, "menu:", 5))
		{
			set = AS_MENU;
			linestart+=5;
		}
		else if (!strncasecmp(linestart, "game:", 5))
		{
			set = AS_GAME;
			linestart+=5;
		}
		else if (!strncasecmp(linestart, "common:", 7))
		{	//the default, but nice to be able to be explicit (especially if you want a colon in the action name).
			set = AS_COMMON;
			linestart+=7;
		}

		cmdfuncs->TokenizeString(linestart);
		if (cmdfuncs->Argc())
		{
			cmdfuncs->Argv(0, name, sizeof(name));

			if (!strcasecmp(name, "dev"))
			{
				cmdfuncs->Argv(1, root, sizeof(root));
				continue;
			}
			else if (!strcasecmp(name, "profile"))
			{
				if (acts)
				{
					suggestedbindings.interactionProfile = profilepath;
					suggestedbindings.countSuggestedBindings = acts;
					suggestedbindings.suggestedBindings = bindings;
					res = xrSuggestInteractionProfileBindings(xr.instance, &suggestedbindings);
					if (XR_FAILED(res))
						Con_Printf(CON_ERROR"%s: xrSuggestInteractionProfileBindings failed - %s\n", fname, XR_StringForResult(res));
					totalacts += acts;
					acts = 0;
				}

				cmdfuncs->Argv(1, name, sizeof(name));
				for (p = 0; p < countof(prefix); p++)
					cmdfuncs->Argv(2+p, prefix[p], sizeof(prefix[p]));
				*root = 0;
				if (XR_FAILED(xrStringToPath(xr.instance, name, &profilepath)))
					profilepath = XR_NULL_PATH;
				continue;
			}
			else if (!strcasecmp(name, "action"))
			{
				cmdfuncs->Argv(1, name, sizeof(name));
				cmdfuncs->Argv(2, desc, sizeof(desc));
				cmdfuncs->Argv(3, type, sizeof(type));
				cmdfuncs->Argv(4, bind, sizeof(bind));
			}
			else if (cmdfuncs->Argc() == 2)
			{
				*desc = 0;
				*type = 0;
				cmdfuncs->Argv(1, bind, sizeof(desc));
			}
			else
			{
				if (cmdfuncs->Argc() < 4)
				{
					Con_Printf("Unknown action command \"%s\"\n", name);
					continue;
				}
				cmdfuncs->Argv(1, desc, sizeof(desc));
				cmdfuncs->Argv(2, type, sizeof(type));
				cmdfuncs->Argv(3, bind, sizeof(bind));
				if (cmdfuncs->Argc() >= 5)
					Con_Printf("%s: %s: Extra tokens found\n", fname, name);
			}

			if (*type)
			{
				XrActionType xrtype;
				if (!strcasecmp(type, "button"))
					xrtype = XR_ACTION_TYPE_BOOLEAN_INPUT;
				else if (!strcasecmp(type, "float"))
					xrtype = XR_ACTION_TYPE_FLOAT_INPUT;
				else if (!strcasecmp(type, "vector2f"))
					xrtype = XR_ACTION_TYPE_VECTOR2F_INPUT;
				else if (!strcasecmp(type, "pose"))
					xrtype = XR_ACTION_TYPE_POSE_INPUT;
				else if (!strcasecmp(type, "vibration") || !strcasecmp(type, "haptic"))
					xrtype = XR_ACTION_TYPE_VIBRATION_OUTPUT;
				else
					continue;

				//define our action...
				act = XR_DefineAction(set, xrtype, name, desc, root);
			}
			else
			{
				act = XR_DefineAction(set, (XrActionType)0, name, NULL, root);
				if(act==XR_NULL_HANDLE)
					Con_Printf("Action %s not defined yet\n", name);
			}

			//and add it to the profile we're building.
			if (act != XR_NULL_HANDLE && *bind && profilepath!=XR_NULL_PATH)
			{
				if (*bind == '/')
				{
					res = xrStringToPath(xr.instance, bind, &bindings[acts].binding);
					if (XR_SUCCEEDED(res) && acts < countof(bindings))
						bindings[acts++].action = act;
				}
				else if (*root == '/')
				{
					res = xrStringToPath(xr.instance, va("%s/%s", root, bind), &bindings[acts].binding);
					if (XR_SUCCEEDED(res) && acts < countof(bindings))
						bindings[acts++].action = act;
				}
				else for (p = 0; p < countof(prefix) && *prefix[p]=='/'; p++)
				{
					res = xrStringToPath(xr.instance, va("%s%s", prefix[p], bind), &bindings[acts].binding);
					if (XR_SUCCEEDED(res) && acts < countof(bindings))
						bindings[acts++].action = act;
				}
			}
		}
	}

	if (acts)
	{
		suggestedbindings.interactionProfile = profilepath;
		suggestedbindings.countSuggestedBindings = acts;
		suggestedbindings.suggestedBindings = bindings;
		res = xrSuggestInteractionProfileBindings(xr.instance, &suggestedbindings);
		if (XR_FAILED(res))
			Con_Printf(CON_ERROR"%s: xrSuggestInteractionProfileBindings failed - %s\n", fname, XR_StringForResult(res));
		totalacts += acts;
	}
	return totalacts;
}

static int QDECL XR_BindProfileFile(const char *fname, qofs_t fsize, time_t mtime, void *ctx, struct searchpathfuncs_s *package)
{
	vfsfile_t *f = fsfuncs->OpenVFS(fname, "rb", FS_GAME);
	if (f)
	{
		size_t len = VFS_GETLEN(f);
		char *buf = malloc(len+1);
		VFS_READ(f, buf, len);
		buf[len] = 0;
		*(unsigned int*)ctx += XR_BindProfileStr(fname, buf);
		free(buf);
		VFS_CLOSE(f);
	}
	return true;
}

static const struct
{
	const char *name;
	const char *script;
} xr_knownprofiles[] =
{
	//FIXME: set up some proper bindings!
	{"khr_simple",		"profile /interaction_profiles/khr/simple_controller    /user/hand/left/ /user/hand/right/\n"
			"dev /user/hand/left\n"
				"+attack_left			\"Left Attack\"		button	input/select/click\n"
				"+menu_left				\"Left Menu\"		button	input/menu/click\n"
				"#POSE_LEFT				\"Left Aim Pose\"	pose	input/aim/pose\n"
				//"left_grip			\"Left Grip Pose\"	pose	input/grip/pose\n"
				"haptic_left			\"Left Haptic\"		haptic	output/haptic\n"
			"dev /user/hand/right\n"
				"menu: #GP_START		\"Menu Enter\"		button	input/select/click\n"
				"menu: #GP_BACK			\"Menu Escape\"		button	input/menu/click\n"
				"game: +attack_right	\"Right Attack\"	button	input/select/click\n"
				"game: +menu_right		\"Right Menu\"		button	input/menu/click\n"
				"#POSE_RIGHT			\"Right Aim Pose\"	pose	input/aim/pose\n"
				//"right_grip			\"Right Grip Pose\"	pose	input/grip/pose\n"
				"haptic_right			\"Right Haptic\"	haptic	output/haptic\n"
	},

/*	{"valve_index",		"profile /interaction_profiles/valve/index_controller    /user/hand/left/ /user/hand/right/\n"
			//"unbound		\"Unused Button\"		button		input/system/click\n"
			//"unbound		\"Unused Button\"		button		input/system/touch\n"
			//"unbound		\"Unused Button\"		button		input/a/click\n"
			//"unbound		\"Unused Button\"		button		input/a/touch\n"
			//"unbound		\"Unused Button\"		button		input/b/click\n"
			//"unbound		\"Unused Button\"		button		input/b/touch\n"
			//"unbound		\"Unused Button\"		float		input/squeeze/value\n"
			//"unbound		\"Unused Button\"		button		input/squeeze/force\n"
			//"unbound		\"Unused Button\"		button		input/trigger/click\n"
			//"unbound		\"Unused Button\"		float		input/trigger/value\n"
			//"unbound		\"Unused Button\"		button		input/trigger/touch\n"
			//"unbound		\"Unused Button\"		vector2f	input/thumbstick\n"
			//"unbound		\"Unused Button\"		button		input/thumbstick/click\n"
			//"unbound		\"Unused Button\"		button		input/thumbstick/touch\n"
			//"unbound		\"Unused Button\"		vector2f	input/trackpad\n"
			//"unbound		\"Unused Button\"		button		input/trackpad/force\n"
			//"unbound		\"Unused Button\"		button		input/trackpad/touch\n"
			//"unbound		\"Unused Button\"		pose		input/grip/pose\n"
			//"unbound		\"Unused Button\"		pose		input/aim/pose\n"
			//"unbound		\"Unused Button\"		haptic		output/haptic\n"
	},
*/
/*	{"htc_vive",		"profile /interaction_profiles/htc/vive_controller    /user/hand/left/ /user/hand/right/\n"
			//"unbound		\"Unused Button\"		button		input/system/click\n"
			//"unbound		\"Unused Button\"		button		input/squeeze/click\n"
			//"unbound		\"Unused Button\"		button		input/menu/click\n"
			//"unbound		\"Unused Button\"		button		input/trigger/click\n"
			//"unbound		\"Unused Button\"		float		input/trigger/value\n"
			//"unbound		\"Unused Button\"		vector2f	input/trackpad\n"
			//"unbound		\"Unused Button\"		button		input/trackpad/click\n"
			//"unbound		\"Unused Button\"		button		input/trackpad/touch\n"
			//"unbound		\"Unused Button\"		pose		input/grip/pose\n"
			//"unbound		\"Unused Button\"		pose		input/aim/pose\n"
			//"unbound		\"Unused Button\"		haptic		output/haptic\n"
			);
*/
/*	{"htc_vive_pro",	"profile /interaction_profiles/htc/vive_pro    /user/head/\n"
			//"unbound		\"Unused Button\"		button		input/system/click\n"
			//"unbound		\"Unused Button\"		button		input/volume_up/click\n"
			//"unbound		\"Unused Button\"		button		input/volume_down/click\n"
			//"unbound		\"Unused Button\"		button		input/mute_mic/click\n"
			);
*/

	//just map everything to quake's various buttons so that mods with proper gamepad mapping will work here too.
	{"gamepad", "profile /interaction_profiles/microsoft/xbox_controller    /user/gamepad/\n"
			"#GP_START				\"Start\"				button		input/menu/click\n"
			"#GP_BACK				\"Back\"				button		input/view/click\n"
			"#GP_A					\"A Button\"			button		input/a/click\n"
			"#GP_B					\"B Button\"			button		input/b/click\n"
			"#GP_X					\"X Button\"			button		input/x/click\n"
			"#GP_Y					\"Y Button\"			button		input/y/click\n"
			"#GP_DPAD_DOWN			\"Move Backwards\"		button		input/dpad_down/click\n"
			"#GP_DPAD_RIGHT			\"Move Right\"			button		input/dpad_right/click\n"
			"#GP_DPAD_UP			\"Move Forward\"		button		input/dpad_up/click\n"
			"#GP_DPAD_LEFT			\"Move Left\"			button		input/dpad_left/click\n"
			"#GP_LSHOULDER			\"Jump\"				button		input/shoulder_left/click\n"
			"#GP_RSHOULDER			\"Attack\"				button		input/shoulder_right/click\n"
			"#GP_LTHUMB				\"Left Thumb\"			button		input/thumbstick_left/click\n"
			"#GP_RTHUMB				\"Right Thumb\"			button		input/thumbstick_right/click\n"
			"#GP_AXIS_LTRIGGER		\"Left Trigger\"		float		input/trigger_left/value\n"
			"#GP_AXIS_RTRIGGER		\"Right Trigger\"		float		input/trigger_right/value\n"
			"#GP_AXIS_LEFT_X		\"Left Thumbstick X\"	float		input/thumbstick_left/x\n"
			"#GP_AXIS_LEFT_Y		\"Left Thumbstick y\"	float		input/thumbstick_left/y\n"
			"#GP_AXIS_RIGHT_X		\"Right Thumbstick X\"	float		input/thumbstick_right/x\n"
			"#GP_AXIS_RIGHT_X		\"Right Thumbstick Y\"	float		input/thumbstick_right/y\n"
			"haptic_gp_left			\"Left Haptic (Main)\"		haptic	output/haptic_left\n"
			"haptic_gp_left_trigger	\"Left-Trigger Haptic\"		haptic	output/haptic_left_trigger\n"
			"haptic_gp_right		\"Right Haptic (Main)\"		haptic	output/haptic_right\n"
			"haptic_gp_right_trigger \"Right-Trigger Haptic\"	haptic	output/haptic_right_trigger\n"
	},
};

static void XR_SetupInputs_Instance(void)
{
	unsigned int h;
	XrResult res;
	int set;
	char *actionsetNames[] = {"genericactions", "menuactions", "gameactions"};
	char *actionsetNamesText[] = {"Generic Actions", "Menu Actions", "Game Actions"};

	for (set = 0; set < MAX_ACTIONSETS; set++)
	{
		XrActionSetCreateInfo info = {XR_TYPE_ACTION_SET_CREATE_INFO};
		Q_strlcpy(info.actionSetName, actionsetNames[set], sizeof(info.actionSetName));
		Q_strlcpy(info.localizedActionSetName, actionsetNamesText[set], sizeof(info.localizedActionSetName));
		info.priority = 0;

		xr.actionset[set].subactionPath = XR_NULL_PATH;
		res = xrCreateActionSet(xr.instance, &info, &xr.actionset[set].actionSet);
		if (XR_FAILED(res))
			Con_Printf("openxr: Unable to create actionset - %s\n", XR_StringForResult(res));
	}

	h = 0;
	if (fsfuncs)
		fsfuncs->EnumerateFiles("oxr_*.binds", XR_BindProfileFile, &h);
	if (!h)	//no user bindings defined, use fallbacks. probably this needs to be per-mod.
	{
		for (h = 0; h < countof(xr_knownprofiles); h++)
			XR_BindProfileStr(xr_knownprofiles[h].name, xr_knownprofiles[h].script);
	}
}
static void XR_SetupInputs_Session(void)
{
	unsigned int h;
	XrResult res;

	//create action space stuff.
	for (h = 0; h < xr.numactions; h++)
	{
		switch(xr.actions[h].acttype)
		{
		case XR_ACTION_TYPE_POSE_INPUT:
			{
				XrActionSpaceCreateInfo info = {XR_TYPE_ACTION_SPACE_CREATE_INFO};
				info.action = xr.actions[h].action;
				info.subactionPath = xr.actions[h].path;
				info.poseInActionSpace.orientation.w = 1;	//just fill with identity.

				res = xrCreateActionSpace(xr.session, &info, &xr.actions[h].space);
				if (XR_FAILED(res))
					Con_Printf("openxr: xrCreateActionSpace failed - %s\n", XR_StringForResult(res));
			}
			break;
		default:
			xr.actions[h].space = XR_NULL_HANDLE;
			break;
		}
	}

	//and attach it.
	{
		XrSessionActionSetsAttachInfo info = {XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO};
		XrActionSet sets[MAX_ACTIONSETS];
		unsigned int set;
		for (set = 0; set < MAX_ACTIONSETS; set++)
			if (xr.actionset[set].actionSet)
				sets[info.countActionSets++] = xr.actionset[set].actionSet;
		info.actionSets = sets;
		res = xrAttachSessionActionSets(xr.session, &info);
		if (XR_FAILED(res))
			Con_Printf("openxr: xrAttachSessionActionSets failed - %s\n", XR_StringForResult(res));
	}

#ifdef XR_EXT_hand_tracking
	//create some hand trackers... try to create one for each hand...
	//(note: this is more a finger-joint tracker than a hand tracker, though limited controllers generally mean its good only for fingers)
	if (xrCreateHandTrackerEXT)
	for (h = 0; h < 2; h++)
	{
		XrHandTrackerCreateInfoEXT info = {XR_TYPE_HAND_TRACKER_CREATE_INFO_EXT};
		info.hand = h?XR_HAND_RIGHT_EXT:XR_HAND_LEFT_EXT;
		info.handJointSet = XR_HAND_JOINT_SET_DEFAULT_EXT;
		res = xrCreateHandTrackerEXT(xr.session, &info, &xr.hand[h].handle);
		if (XR_FAILED(res))
			Con_Printf("openxr: xrCreateHandTrackerEXT failed - %s\n", XR_StringForResult(res));
	}
#endif
}

static void XR_PrintInputs(void)
{
	XrResult res;
	XrInteractionProfileState profile = {XR_TYPE_INTERACTION_PROFILE_STATE};
	XrPath path;
	unsigned int u;
	static const char *paths[] = {"/user/hand/left", "/user/hand/right", "/user/head", "/user/gamepad"};
	Con_Printf("OpenXR Interaction Profiles:\n");
	for (u = 0; u < countof(paths); u++)
	{
		xrStringToPath(xr.instance, paths[u], &path);
		res = xrGetCurrentInteractionProfile(xr.session, path, &profile);
		if (XR_SUCCEEDED(res))
		{
			char buf[256];
			uint32_t len = sizeof(buf);
			if (!profile.interactionProfile)
				Con_Printf("\t%s: "S_COLOR_GRAY"no profile/device\n", paths[u]);
			else
			{
				res = xrPathToString(xr.instance, profile.interactionProfile, sizeof(buf), &len, buf);
				Con_Printf("\t%s: "S_COLOR_GREEN"%s\n", paths[u], buf);
			}
		}
	}


	Con_Printf("Bound actions:\n");
	for (u = 0; u < xr.numactions; u++)
	{
		XrBoundSourcesForActionEnumerateInfo info = {XR_TYPE_BOUND_SOURCES_FOR_ACTION_ENUMERATE_INFO};
		uint32_t inputs, i, bufsize;
		XrPath input[20];
		info.action = xr.actions[u].action;
		res = xrEnumerateBoundSourcesForAction(xr.session, &info, countof(input), &inputs, input);
		if (XR_SUCCEEDED(res))
		{
			Con_Printf("\t%s "S_COLOR_GRAY"(%s)"S_COLOR_WHITE":\n", xr.actions[u].actname, xr.actions[u].actdescription);
			if (!inputs)
				Con_Printf(S_COLOR_GRAY"\t\t(unbound)\n");
			else for (i = 0; i < inputs; i++)
			{
				char buffer[8192];
				XrInputSourceLocalizedNameGetInfo info = {XR_TYPE_INPUT_SOURCE_LOCALIZED_NAME_GET_INFO};
				info.sourcePath = input[i];
				info.whichComponents =	XR_INPUT_SOURCE_LOCALIZED_NAME_USER_PATH_BIT|	//'left hand'
										XR_INPUT_SOURCE_LOCALIZED_NAME_INTERACTION_PROFILE_BIT|	//'foo controller'
										XR_INPUT_SOURCE_LOCALIZED_NAME_COMPONENT_BIT;	//'trigger'
				res = xrGetInputSourceLocalizedName(xr.session, &info, sizeof(buffer), &bufsize, buffer);
				if (XR_FAILED(res))
					Q_snprintf(buffer, sizeof(buffer), S_COLOR_RED"error %i", res);
				else
					Con_Printf(S_COLOR_GREEN"\t\t%s\n", buffer);
			}
		}
		else if (res == XR_ERROR_HANDLE_INVALID)	//monado reports this for unimplemented things.
			Con_Printf(S_COLOR_RED"\t%s: error XR_ERROR_HANDLE_INVALID (not implemented?)\n", xr.actions[u].actname);
		else
			Con_Printf(S_COLOR_RED"\t%s: error %s\n", xr.actions[u].actname, XR_StringForResult(res));
	}
}

static void XR_UpdateInputs(XrTime time)
{
	XrResult res;
	size_t h;
	qboolean activesets[MAX_ACTIONSETS] = {true};

	if (inputfuncs->GetKeyDest() & ~kdm_game)
		activesets[AS_MENU] = true;	//used while looking at the menuqc/emenus/console/etc.
	else
		activesets[AS_GAME] = true;	//used while the game has focus.

	{
		XrActionsSyncInfo syncinfo = {XR_TYPE_ACTIONS_SYNC_INFO};
		XrActiveActionSet sets[MAX_ACTIONSETS];
		for (h = 0; h < MAX_ACTIONSETS; h++)
			if (activesets[h])
				sets[syncinfo.countActiveActionSets++] = xr.actionset[h];
		syncinfo.activeActionSets = sets;
		res = xrSyncActions(xr.session, &syncinfo);
		if (res == XR_SESSION_NOT_FOCUSED)
			;	//handle it anyway, giving us a chance to disable various inputs.
		else if (XR_FAILED(res))
			return;
	}

	for (h = 0; h < xr.numactions; h++)
	{
		if (xr.actions[h].action == XR_NULL_HANDLE)	//failed to init
			continue;
		safeswitch(xr.actions[h].acttype)
		{
		case XR_ACTION_TYPE_POSE_INPUT:
			{
				XrActionStatePose pose = {XR_TYPE_ACTION_STATE_POSE};
				XrActionStateGetInfo info = {XR_TYPE_ACTION_STATE_GET_INFO};
				info.action = xr.actions[h].action;
				info.subactionPath = xr.actions[h].path;

				if (XR_FAILED(xrGetActionStatePose(xr.session, &info, &pose)))
					break;
				if (pose.isActive && activesets[xr.actions[h].set])
				{	//its mapped to something, woo.
					XrSpaceVelocity vel = {XR_TYPE_SPACE_VELOCITY};
					XrSpaceLocation loc = {XR_TYPE_SPACE_LOCATION, &vel};
					vec3_t angles, org, lvel, avel;
					res = xrLocateSpace(xr.actions[h].space, xr.space, time, &loc);
					XR_PoseToAngOrg(&loc.pose, angles, org);

					VectorSet(lvel, vel.linearVelocity.x, vel.linearVelocity.y, vel.linearVelocity.z);
					VectorSet(avel, vel.angularVelocity.x, vel.angularVelocity.y, vel.angularVelocity.z);
					if (!strncasecmp(xr.actions[h].actname, "#POSE_", 6))
					{
						if (inputfuncs->SetHandPosition(xr.actions[h].actname+6,
								(loc.locationFlags&XR_SPACE_LOCATION_POSITION_VALID_BIT)?org:NULL,
								(loc.locationFlags&XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)?angles:NULL,
								(vel.velocityFlags&XR_SPACE_VELOCITY_LINEAR_VALID_BIT)?lvel:NULL,
								(vel.velocityFlags&XR_SPACE_VELOCITY_ANGULAR_VALID_BIT)?avel:NULL))
							;
						else
							Con_Printf("Pose \"%s\" for action \"%s\" is not a known pose name\n", xr.actions[h].actname+1, xr.actions[h].actdescription);
					}
					else
					{	//custom poses that mods might want to handle themselves...
						char cmd[256];
						unsigned int status = 0;
						status |= (loc.locationFlags&XR_SPACE_LOCATION_POSITION_VALID_BIT)?VRSTATUS_ORG:0;
						status |= (loc.locationFlags&XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)?VRSTATUS_ANG:0;
						status |= (vel.velocityFlags&XR_SPACE_VELOCITY_LINEAR_VALID_BIT)?VRSTATUS_VEL:0;
						status |= (vel.velocityFlags&XR_SPACE_VELOCITY_ANGULAR_VALID_BIT)?VRSTATUS_AVEL:0;
						if (status & (VRSTATUS_VEL|VRSTATUS_AVEL))
						{
							Q_snprintf(cmd, sizeof(cmd), "%s %u %g %g %g %g %g %g %g %g %g %g %g %g\n", xr.actions[h].actname, status,
											 angles[0], angles[1], angles[2], org[0], org[1], org[2],
											 vel.angularVelocity.x, vel.angularVelocity.y, vel.angularVelocity.z, vel.linearVelocity.x, vel.linearVelocity.y, vel.linearVelocity.z);
						}
						else if (status & VRSTATUS_ORG)
						{
							Q_snprintf(cmd, sizeof(cmd), "%s %u %g %g %g %g %g %g\n", xr.actions[h].actname, status,
											 angles[0], angles[1], angles[2], org[0], org[1], org[2]);
						}
						else
						{
							Q_snprintf(cmd, sizeof(cmd), "%s %u %g %g %g\n", xr.actions[h].actname, status,
											 angles[0], angles[1], angles[2]);
						}
						cmdfuncs->AddText(cmd, false);
					}
				}
			}
			break;
		case XR_ACTION_TYPE_BOOLEAN_INPUT:
			{
				XrActionStateBoolean state = {XR_TYPE_ACTION_STATE_BOOLEAN};
				XrActionStateGetInfo info = {XR_TYPE_ACTION_STATE_GET_INFO};
				info.action = xr.actions[h].action;
				info.subactionPath = xr.actions[h].path;
				if (XR_FAILED(xrGetActionStateBoolean(xr.session, &info, &state)))
					break;
				if (!state.isActive) state.currentState = XR_FALSE;
				if ((!!state.currentState) != xr.actions[h].held)
				{
					xr.actions[h].held = !!state.currentState;
					if (xr.actions[h].held && !activesets[xr.actions[h].set])
						break;	//don't fire the down event when this action's set isn't meant to be active...
					if (*xr.actions[h].actname == '#')
					{
						int k = inputfuncs->GetKeyCode(xr.actions[h].actname+1, NULL);
						if (k >= 0)
							inputfuncs->KeyEvent(0, xr.actions[h].held, k, 0);
						else
							Con_Printf("Key \"%s\" for action \"%s\" is not a known key code\n", xr.actions[h].actname+1, xr.actions[h].actdescription);
					}
					else if (xr.actions[h].held || *xr.actions[h].actname == '+')
					{
						char cmd[256];
						Q_strlcpy(cmd, xr.actions[h].actname, sizeof(cmd));
						Q_strlcat(cmd, "\n", sizeof(cmd));
						if (!xr.actions[h].held)
							*cmd = '-';	//release events.
						cmdfuncs->AddText(cmd, false);
					}
				}
			}
			break;
		case XR_ACTION_TYPE_FLOAT_INPUT:
			{
				XrActionStateFloat state = {XR_TYPE_ACTION_STATE_FLOAT};
				XrActionStateGetInfo info = {XR_TYPE_ACTION_STATE_GET_INFO};
				info.action = xr.actions[h].action;
				info.subactionPath = xr.actions[h].path;
				if (XR_FAILED(xrGetActionStateFloat(xr.session, &info, &state)))
					break;

				if (state.isActive && activesets[xr.actions[h].set])
				{
					char cmd[256];
					if (!strncasecmp(xr.actions[h].actname, "#GP_AXIS_", 9))
					{
						int axis = -1;
						if (!strcasecmp(xr.actions[h].actname+9, "LTRIGGER"))
							axis = GPAXIS_LT_TRIGGER;
						else if (!strcasecmp(xr.actions[h].actname+9, "RTRIGGER"))
							axis = GPAXIS_RT_TRIGGER;
						else if (!strcasecmp(xr.actions[h].actname+9, "LEFT_X"))
							axis = GPAXIS_LT_RIGHT;
						else if (!strcasecmp(xr.actions[h].actname+9, "LEFT_Y"))
							axis = GPAXIS_LT_DOWN;
						else if (!strcasecmp(xr.actions[h].actname+9, "RIGHT_X"))
							axis = GPAXIS_RT_RIGHT;
						else if (!strcasecmp(xr.actions[h].actname+9, "RIGHT_Y"))
							axis = GPAXIS_RT_DOWN;
						else
							Con_Printf("Unknown gamepad axis: \"%s\"\n", xr.actions[h].actname+1);
						inputfuncs->JoystickAxisEvent(0, axis, state.currentState);
					}
					else
					{
						Q_snprintf(cmd, sizeof(cmd), "%s %g\n", xr.actions[h].actname, state.currentState);
						cmdfuncs->AddText(cmd, false);
					}
				}
			}
			break;
		case XR_ACTION_TYPE_VECTOR2F_INPUT:
			{
				XrActionStateVector2f state = {XR_TYPE_ACTION_STATE_VECTOR2F};
				XrActionStateGetInfo info = {XR_TYPE_ACTION_STATE_GET_INFO};
				info.action = xr.actions[h].action;
				info.subactionPath = xr.actions[h].path;
				if (XR_FAILED(xrGetActionStateVector2f(xr.session, &info, &state)))
					break;

				if (state.isActive && activesets[xr.actions[h].set])
				{
					char cmd[256];
					Q_snprintf(cmd, sizeof(cmd), "%s %g %g\n", xr.actions[h].actname, state.currentState.x, state.currentState.y);
					cmdfuncs->AddText(cmd, false);
				}
			}
			break;
		case XR_ACTION_TYPE_VIBRATION_OUTPUT:	//output only, nothing to read.
		case XR_ACTION_TYPE_MAX_ENUM: //not a real value
		safedefault:
			break;
		}
	}
}

static int64_t XR_CheckSwapFormats(int64_t *tryformat, int64_t *fmts, size_t fmtcount)
{
	size_t i, j;
	for (j = 0; j < fmtcount; j++)
	{	//the openxr driver lists is supposed to list formats in the order that it favours, so favour those.
		for (i = 0; tryformat[i]; i++)
		{	//now make sure its one that we allow. (steamvr lists ones that are NOT renderable - even though they should be)
			if (tryformat[i] == fmts[j])
				return fmts[j];
		}
	}
	return 0;
}
static int64_t XR_FindSwapFormat(int64_t **tryformats, int64_t *fmts, size_t fmtcount)
{
	int64_t fmt;
	/*try to use a format that matches the user's choice*/
	if (xr.srgb == 2)
		fmt = XR_CheckSwapFormats(tryformats[0], fmts, fmtcount);
	else if (xr.srgb)
		fmt = XR_CheckSwapFormats(tryformats[1], fmts, fmtcount);
	else
		fmt = XR_CheckSwapFormats(tryformats[2], fmts, fmtcount);

	/*try others out of desperation*/
	if (!fmt)
		fmt = XR_CheckSwapFormats(tryformats[1], fmts, fmtcount);
	if (!fmt)
		fmt = XR_CheckSwapFormats(tryformats[0], fmts, fmtcount);
	if (!fmt)
		fmt = XR_CheckSwapFormats(tryformats[2], fmts, fmtcount);
	if (!fmt)
		fmt = tryformats[1][0];	//fall back on the first srgb format we know of, in the hope that the driver just failed to list it.
	return fmt;
}
static qboolean XR_Begin(void)
{
	uint32_t u;
	XrResult res;
	uint32_t swapfmts;
	int64_t *fmts, fmttouse=0;

	xr.beginning = false;
	xr.ending = false;
	xr.inputsdirty = true;

	{
		XrSessionCreateInfo sessioninfo = {XR_TYPE_SESSION_CREATE_INFO};
		sessioninfo.next = xr.bindinginfo;
		sessioninfo.createFlags = 0;
		sessioninfo.systemId = xr.systemid;
		res = xrCreateSession(xr.instance, &sessioninfo, &xr.session);
	}
	if (XR_FAILED(res))
	{
		Con_Printf("OpenXR: xrCreateSession failed (%s)\n", XR_StringForResult(res));
		return false;
	}

	{
		XrReferenceSpaceCreateInfo info = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
		info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
		info.poseInReferenceSpace.orientation.w = 1;
		res = xrCreateReferenceSpace(xr.session, &info, &xr.space);
		if (XR_FAILED(res))
			return false;
	}

	xrEnumerateSwapchainFormats(xr.session, 0, &swapfmts, NULL);
	fmts = alloca(sizeof(*fmts)*swapfmts);
	res = xrEnumerateSwapchainFormats(xr.session, swapfmts, &swapfmts, fmts);
	if (xr.renderer == QR_HEADLESS)
		;
	else if (!swapfmts)
		Con_Printf("OpenXR: No swapchain formats to use (%s)\n", XR_StringForResult(res));
#ifdef XR_USE_GRAPHICS_API_OPENGL
	else if (xr.renderer == QR_OPENGL)
	{
		static int64_t hdrformats[] = {GL_RGBA16F,GL_RGBA32F, 0};
		static int64_t srgbformats[] = {GL_SRGB8_ALPHA8_EXT, GL_SRGB8_EXT, 0};
		static int64_t rgbformats[] = {GL_RGB10_A2, GL_RGBA8, /*broken on steamvr - GL_RGBA16_EXT,*/ GL_RGB8, 0};
		static int64_t *formats[] = {hdrformats, srgbformats, rgbformats};
		fmttouse = XR_FindSwapFormat(formats, fmts, swapfmts);
	}
#endif
#ifdef XR_USE_GRAPHICS_API_VULKAN
	else if (xr.renderer == QR_VULKAN)
	{
		static int64_t hdrformats[] = {VK_FORMAT_R16G16B16A16_SFLOAT,VK_FORMAT_R32G32B32A32_SFLOAT, VK_FORMAT_R16G16B16_SFLOAT,VK_FORMAT_R32G32B32_SFLOAT, 0};
		static int64_t srgbformats[] = {VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_R8G8B8_SRGB, VK_FORMAT_B8G8R8_SRGB, 0};
		static int64_t rgbformats[] = {VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8_UNORM, VK_FORMAT_B8G8R8_UNORM, 0};
		static int64_t *formats[] = {hdrformats, srgbformats, rgbformats};
		fmttouse = XR_FindSwapFormat(formats, fmts, swapfmts);
	}
#endif
#ifdef XR_USE_GRAPHICS_API_D3D11
	else if (xr.renderer == QR_DIRECT3D11)
	{
		static int64_t hdrformats[] = {DXGI_FORMAT_R16G16B16A16_FLOAT, 0};
		static int64_t srgbformats[] = {DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, DXGI_FORMAT_B8G8R8X8_UNORM_SRGB, 0};
		static int64_t rgbformats[] = {DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8X8_UNORM, 0};
		static int64_t *formats[] = {hdrformats, srgbformats, rgbformats};
		fmttouse = XR_FindSwapFormat(formats, fmts, swapfmts);
	}
#endif
	else
	{
		fmttouse = fmts[0];
		for (u = 0; u < swapfmts; u++)
			Con_Printf("fmt%u: %u / %x\n", u, (unsigned)fmts[u], (unsigned)fmts[u]);
	}

	xr.colourformat = fmttouse;
	for (u = 0; u < xr.viewcount; u++)
	{
		XrSwapchainCreateInfo swapinfo = {XR_TYPE_SWAPCHAIN_CREATE_INFO};
		swapinfo.createFlags = 0;
		swapinfo.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
		swapinfo.format = fmttouse;
		swapinfo.sampleCount = 1;//xr.views->recommendedSwapchainSampleCount;
		swapinfo.width = xr.views->recommendedImageRectWidth;
		swapinfo.height = xr.views->recommendedImageRectHeight;
		swapinfo.faceCount = 1;	//2d, not a cube
		swapinfo.arraySize = 1;	//1 for 2d, a 1d array isn't allowed
		swapinfo.mipCount = 1;
		res = xrCreateSwapchain(xr.session, &swapinfo, &xr.eye[u].swapchain);
		if (XR_FAILED(res))
		{
			Con_Printf("OpenXR: xrCreateSwapchain failed (%s)\n", XR_StringForResult(res));
			return false;
		}
		res = xrEnumerateSwapchainImages(xr.eye[u].swapchain, 0, &xr.eye[u].numswapimages, NULL);
		if (XR_FAILED(res))
		{
			Con_Printf("OpenXR: xrEnumerateSwapchainImages failed (%s)\n", XR_StringForResult(res));
			return false;
		}

		//using a separate swapchain for each eye, so just depend upon npot here and use the whole image.
		xr.eye[u].subimage.imageRect.offset.x = 0;
		xr.eye[u].subimage.imageRect.offset.y = 0;
		xr.eye[u].subimage.imageRect.extent.width = swapinfo.width;
		xr.eye[u].subimage.imageRect.extent.height = swapinfo.height;
		xr.eye[u].subimage.swapchain = xr.eye[u].swapchain;
		xr.eye[u].subimage.imageArrayIndex = 0;

		//okay, this is annoying. the returned array size has different strides for different apis, etc.
		//translate it into something the relevant backend should understand.
		switch(xr.renderer)
		{
		default:
			return false;	//erk?
#ifdef XR_USE_GRAPHICS_API_D3D11
		case QR_DIRECT3D11:
			{
				uint32_t i;
				XrSwapchainImageD3D11KHR *xrimg = calloc(xr.eye[u].numswapimages, sizeof(*xrimg));
				for (i = 0; i < xr.eye[u].numswapimages; i++)
					xrimg[i].type = XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR;
				res = xrEnumerateSwapchainImages(xr.eye[u].swapchain, xr.eye[u].numswapimages, &xr.eye[u].numswapimages, (XrSwapchainImageBaseHeader*)xrimg);
				if (XR_FAILED(res))
					xr.eye[u].numswapimages = 0;

				xr.eye[u].swapimages = calloc(xr.eye[u].numswapimages, sizeof(*xr.eye[u].swapimages));
				for (i = 0; i < xr.eye[u].numswapimages; i++)
				{
					xr.eye[u].swapimages[i].ptr = xrimg[i].texture;
					xr.eye[u].swapimages[i].ptr2 = NULL;	//view
					xr.eye[u].swapimages[i].width = swapinfo.width;
					xr.eye[u].swapimages[i].height = swapinfo.height;
					xr.eye[u].swapimages[i].depth = 1;
					xr.eye[u].swapimages[i].status = TEX_LOADED;
				}
			}
			break;
#endif
#ifdef XR_USE_GRAPHICS_API_VULKAN
		case QR_VULKAN:
			{
				uint32_t i;
				XrSwapchainImageVulkanKHR *xrimg = calloc(xr.eye[u].numswapimages, sizeof(*xrimg));
				struct vk_image_s *vkimg;
				for (i = 0; i < xr.eye[u].numswapimages; i++)
					xrimg[i].type = XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR;
				res = xrEnumerateSwapchainImages(xr.eye[u].swapchain, xr.eye[u].numswapimages, &xr.eye[u].numswapimages, (XrSwapchainImageBaseHeader*)xrimg);
				if (XR_FAILED(res))
					xr.eye[u].numswapimages = 0;

				xr.eye[u].swapimages = calloc(xr.eye[u].numswapimages, sizeof(*xr.eye[u].swapimages)+sizeof(struct vk_image_s));
				vkimg = (struct vk_image_s*)&xr.eye[u].swapimages[xr.eye[u].numswapimages];
				for (i = 0; i < xr.eye[u].numswapimages; i++)
				{
					xr.eye[u].swapimages[i].vkimage = &vkimg[i];
					vkimg[i].vkformat = fmttouse;
					vkimg[i].image = xrimg[i].image;
					//vkimg[i].mem.* = 0;
					vkimg[i].view = VK_NULL_HANDLE;
					vkimg[i].sampler = VK_NULL_HANDLE;
					vkimg[i].layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
					vkimg[i].width = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
					xr.eye[u].swapimages[i].width = vkimg[i].width = swapinfo.width;
					xr.eye[u].swapimages[i].height = vkimg[i].height = swapinfo.height;
					xr.eye[u].swapimages[i].depth = vkimg[i].layers = 1;
					vkimg[i].mipcount = swapinfo.mipCount;
					vkimg[i].encoding = PTI_INVALID; //blurgh, is this needed?
					vkimg[i].type = PTI_2D;
					xr.eye[u].swapimages[i].status = TEX_LOADED;
				}
			}
			break;
#endif
#ifdef XR_USE_GRAPHICS_API_OPENGL
		case QR_OPENGL:
			{
				uint32_t i;
				XrSwapchainImageOpenGLKHR *xrimg = calloc(xr.eye[u].numswapimages, sizeof(*xrimg));
				for (i = 0; i < xr.eye[u].numswapimages; i++)
					xrimg[i].type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR;
				res = xrEnumerateSwapchainImages(xr.eye[u].swapchain, xr.eye[u].numswapimages, &xr.eye[u].numswapimages, (XrSwapchainImageBaseHeader*)xrimg);
				if (XR_FAILED(res))
					xr.eye[u].numswapimages = 0;

				xr.eye[u].swapimages = calloc(xr.eye[u].numswapimages, sizeof(*xr.eye[u].swapimages));
				for (i = 0; i < xr.eye[u].numswapimages; i++)
				{
					xr.eye[u].swapimages[i].format = swapinfo.format;
					xr.eye[u].swapimages[i].num = xrimg[i].image;
					xr.eye[u].swapimages[i].width = swapinfo.width;
					xr.eye[u].swapimages[i].height = swapinfo.height;
					xr.eye[u].swapimages[i].depth = 1;
					xr.eye[u].swapimages[i].status = TEX_LOADED;
				}
			}
			break;
#endif
		}
	}
	if (XR_FAILED(res))
		return false;

	XR_SetupInputs_Session();

	return true;
}

static void XR_ProcessEvents(void)
{
	XrEventDataBuffer ev;
	XrResult res;
	for (;;)
	{
		ev.type = XR_TYPE_EVENT_DATA_BUFFER;
		ev.next = NULL;
		res = xrPollEvent(xr.instance, &ev);
		if (res == XR_EVENT_UNAVAILABLE || XR_FAILED(res))
			return;	//nothing interesting here folks

		switch(ev.type)
		{
		default:	//no idea wtf that is
			Con_Printf("openxr event %u\n", ev.type);
			break;

		case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING:
			XR_Shutdown();	//we're meant to try restarting, but that's a hassle. FIXME: expect the user to do a vid_restart.
			return;

		case XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING:
			break;
		case XR_TYPE_EVENT_DATA_EVENTS_LOST:
			{
				XrEventDataEventsLost *s = (XrEventDataEventsLost*)&ev;
				Con_Printf(CON_ERROR"OpenXR: Lost %u events!\n", s->lostEventCount);
			}
			break;
		case XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED:
			xr.inputsdirty = true;
			break;

		case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED:
			{
				XrEventDataSessionStateChanged *s = (XrEventDataSessionStateChanged*)&ev;
				xr.state = s->state;
				return;	//make sure the outer loop actually sees each state change.
			}
			break;
		}
	}
}
static qboolean XR_SyncFrame(double *frametime)
{
	XrResult res;

	if (!xr.instance)
		return false;

	if (xr.needrender)
	{	//something screwed up.
//		*frametime = 0;
		return true;
	}

	if (!xr.session)
	{
		if (xr_enable->ival && !XR_Begin())
		{
			XR_Shutdown();
			return false;
		}
	}
	else
	{
		if (!xr_enable->ival && !xr.ending)
		{	//user doesn't want a session apparently. try and end the current session cleanly.
			res = xrRequestExitSession(xr.session);
			if (XR_FAILED(res))
				Con_Printf("openxr: Unable to request session end: %s\n", XR_StringForResult(res));
			xr.ending = true;
		}
	}

	XR_ProcessEvents();

	memset(&xr.framestate, 0, sizeof(xr.framestate));
	xr.framestate.type = XR_TYPE_FRAME_STATE;
	safeswitch(xr.state)
	{
	case XR_SESSION_STATE_IDLE:		//not allowed to progress till the user puts it on their head/etc
		xr.beginning = false;
		break;
	case XR_SESSION_STATE_READY:
		if (!xr.beginning)
		{
			XrSessionBeginInfo info = {XR_TYPE_SESSION_BEGIN_INFO};
			info.primaryViewConfigurationType = xr.viewtype;
			res = xrBeginSession(xr.session, &info);
			if (XR_FAILED(res))
				Con_Printf("Unable to begin session: %s\n", XR_StringForResult(res));
			xr.beginning = true;	//begin our xr loop... (and/or stop the spam just above)
		}
		break;
	case XR_SESSION_STATE_SYNCHRONIZED:	//no rendering or input yet
	case XR_SESSION_STATE_VISIBLE:		//now generating video frames, but no input yet
	case XR_SESSION_STATE_FOCUSED:		//we have inputs! (and still generating video frames)
		break;
	case XR_SESSION_STATE_STOPPING:		//going back to idle (user took it off their head). we'll go back to rendering to our window again.
		xrEndSession(xr.session);
		xr.beginning = false;
		break;
	case XR_SESSION_STATE_LOSS_PENDING:	//terminate for now. recreate later if you want.
		XR_SessionEnded();	//destroys the session but not the instance, so it can be started up again if desired.
		xr.beginning = false;
		break;
	case XR_SESSION_STATE_EXITING:		//terminate with prejudice.
		XR_SessionEnded();
		xr.beginning = false;
		if (!xr.ending)
			XR_Shutdown();	//this doesn't look like one we requested... don't let it start back up again.
		break;
	case XR_SESSION_STATE_UNKNOWN:
	case XR_SESSION_STATE_MAX_ENUM:
	safedefault:
		xr.beginning = false;	//some weird error.
		break;
	}

	if (xr.beginning)
	{
		XrTime time;
		res = xrWaitFrame(xr.session, NULL, &xr.framestate);
		if (XR_FAILED(res))
		{
			Con_Printf("xrWaitFrame: %s\n", XR_StringForResult(res));
			return false;
		}
		time = xr.framestate.predictedDisplayTime;
		if (xr.timeknown)
		{
			if (time < xr.time)	//make sure time doesn't go backward...
				time = xr.time;
			*frametime = NANOSECONDS_TO_SECONDS(time-xr.time);
		}
		xr.time = time;
		xr.timeknown = true;

		xr.needrender = true;
	}

	if (xr.session)
		XR_UpdateInputs(xr.framestate.predictedDisplayTime);

	return true;
}
static qboolean XR_Render(void(*rendereye)(texid_t tex, vec4_t fovoverride, vec3_t angorg[2]))
{
	XrFrameEndInfo endframeinfo = {XR_TYPE_FRAME_END_INFO};
	unsigned int u;
	XrResult res;

	XrCompositionLayerProjection proj = {XR_TYPE_COMPOSITION_LAYER_PROJECTION};
	const XrCompositionLayerBaseHeader *projlist[] = {(XrCompositionLayerBaseHeader*)&proj};
	XrCompositionLayerProjectionView projviews[MAX_VIEW_COUNT];

	if (!xr.instance)
		return false;	//err... noooes!

	if (!xr.session || !xr.beginning)
		return false;

	if (xr.inputsdirty && xr.state==XR_SESSION_STATE_FOCUSED)
	{
		xr.inputsdirty = false;
		if (xr_debug->ival)
			XR_PrintInputs();
	}
#ifdef XR_EXT_hand_tracking
	for (u = 0; u < 2; u++)
	{
		vec3_t ang, org;
		unsigned int j;
		static const char *jointnames[] = {
			"PALM",
			"WRIST",
			"THUMB_METACARPAL",
			"THUMB_PROXIMAL",
			"THUMB_DISTAL",
			"THUMB_TIP",
			"INDEX_METACARPAL",
			"INDEX_PROXIMAL",
			"INDEX_INTERMEDIATE",
			"INDEX_DISTAL",
			"INDEX_TIP",
			"MIDDLE_METACARPAL",
			"MIDDLE_PROXIMAL",
			"MIDDLE_INTERMEDIATE",
			"MIDDLE_DISTAL",
			"MIDDLE_TIP",
			"RING_METACARPAL",
			"RING_PROXIMAL",
			"RING_INTERMEDIATE",
			"RING_DISTAL",
			"RING_TIP",
			"LITTLE_METACARPAL",
			"LITTLE_PROXIMAL",
			"LITTLE_INTERMEDIATE",
			"LITTLE_DISTAL",
			"LITTLE_TIP",
		};
		XrHandJointsLocateInfoEXT info = {XR_TYPE_HAND_JOINTS_LOCATE_INFO_EXT};
		XrHandJointLocationsEXT loc = {XR_TYPE_HAND_JOINT_LOCATIONS_EXT};
		XrHandJointVelocitiesEXT vel = {XR_TYPE_HAND_JOINT_VELOCITIES_EXT};
		loc.next = &vel;
		loc.jointCount = countof(xr.hand[u].jointloc);
		loc.jointLocations = xr.hand[u].jointloc;

		vel.next = &vel;
		vel.jointCount = countof(xr.hand[u].jointvel);
		vel.jointVelocities = xr.hand[u].jointvel;

		if (xr.hand[u].handle)
			xrLocateHandJointsEXT(xr.hand[u].handle, &info, &loc);
		xr.hand[u].active = loc.isActive;

		if (!xr.hand[u].active || !xr_debug->ival)
			continue;
		for (j = 0; j < countof(jointnames); j++)
		{
			if (!xr.hand[u].jointloc[j].locationFlags && !xr.hand[u].jointvel[j].velocityFlags)
				continue;
			XR_PoseToAngOrg(&xr.hand[u].jointloc[j].pose, ang, org);

			Con_Printf("%s %s: (%g %g %g) [%g %g %g] %g (%g %g %g) [%g %g %g]\n", u?"Right":"Left", jointnames[j],
				ang[0],ang[1],ang[2],org[0],org[1],org[2], xr.hand[u].jointloc[j].radius,
				xr.hand[u].jointvel[j].angularVelocity.x,xr.hand[u].jointvel[j].angularVelocity.y,xr.hand[u].jointvel[j].angularVelocity.z,
				xr.hand[u].jointvel[j].linearVelocity.x,xr.hand[u].jointvel[j].linearVelocity.y,xr.hand[u].jointvel[j].linearVelocity.z);
		}
	}
#endif

	if (!xr.needrender)
		return false;	//xrWaitFrame not called?
	xr.needrender = false;

	res = xrBeginFrame(xr.session, NULL);
	if (XR_FAILED(res))
	{
		Con_Printf("xrBeginFrame: %s\n", XR_StringForResult(res));
		if(res == XR_ERROR_SESSION_LOST)
			XR_SessionEnded();
		else
			XR_Shutdown();
		return false;
	}
	if (xr.framestate.shouldRender)
	{
		uint32_t eyecount;
		XrViewState viewstate = {XR_TYPE_VIEW_STATE};
		XrViewLocateInfo locateinfo = {XR_TYPE_VIEW_LOCATE_INFO};
		XrView eyeview[MAX_VIEW_COUNT]={};
		for (u = 0; u < MAX_VIEW_COUNT; u++)
			eyeview[u].type = XR_TYPE_VIEW;

		locateinfo.displayTime = xr.framestate.predictedDisplayTime;
		locateinfo.space = xr.space;
		res = xrLocateViews(xr.session, &locateinfo, &viewstate, xr.viewcount, &eyecount, eyeview);
		if (XR_FAILED(res))
			Con_Printf("xrLocateViews: %s\n", XR_StringForResult(res));

		proj.layerFlags = 0;
		proj.space = xr.space;
		proj.views = projviews;
		endframeinfo.layerCount = 1;

		//set up the head position, as an average of all the eyes, the eyes, the awful knowing eyes...
		{
			float scale;
			vec3_t ang, org;
			XrPosef apose = {0};
			for (u = 0; u < xr.viewcount && u < eyecount; u++)
			{	//add em up
				apose.orientation.x += eyeview[u].pose.orientation.x;
				apose.orientation.y += eyeview[u].pose.orientation.y;
				apose.orientation.z += eyeview[u].pose.orientation.z;
				apose.orientation.w += eyeview[u].pose.orientation.w;
				apose.position.x += eyeview[u].pose.position.x;
				apose.position.y += eyeview[u].pose.position.y;
				apose.position.z += eyeview[u].pose.position.z;
			}
			//normalize them
			scale = 1 / sqrt(apose.orientation.x*apose.orientation.x+apose.orientation.y*apose.orientation.y+apose.orientation.z*apose.orientation.z+apose.orientation.w*apose.orientation.w);
			apose.orientation.x *= scale;
			apose.orientation.y *= scale;
			apose.orientation.z *= scale;
			apose.orientation.w *= scale;
			apose.position.x /= xr.viewcount;
			apose.position.y /= xr.viewcount;
			apose.position.z /= xr.viewcount;
			XR_PoseToAngOrg(&apose, ang, org);
			inputfuncs->SetHandPosition("head", org, ang, NULL, NULL);
		}

		for (u = 0; u < xr.viewcount && u < eyecount; u++)
		{
			vec4_t fovoverride;
			XrSwapchainImageWaitInfo waitinfo = {XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
			unsigned int imgidx = 0;
			vec3_t orientation[2];
			res = xrAcquireSwapchainImage(xr.eye[u].swapchain, NULL, &imgidx);
			if (XR_FAILED(res))
				Con_Printf("xrAcquireSwapchainImage: %s\n", XR_StringForResult(res));

			memset(&projviews[u], 0, sizeof(projviews[u]));
			projviews[u].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW;
			projviews[u].pose = eyeview[u].pose;
			projviews[u].fov = eyeview[u].fov;
			projviews[u].subImage = xr.eye[u].subimage;

			XR_PoseToAngOrg(&eyeview[u].pose, orientation[0], orientation[1]);

			fovoverride[0] = eyeview[u].fov.angleLeft * (180/M_PI);
			fovoverride[1] = eyeview[u].fov.angleRight * (180/M_PI);
			fovoverride[2] = eyeview[u].fov.angleDown * (180/M_PI);
			fovoverride[3] = eyeview[u].fov.angleUp * (180/M_PI);

			waitinfo.timeout = SECONDS_TO_NANOSECONDS(0.1);
			res = xrWaitSwapchainImage(xr.eye[u].swapchain, &waitinfo);
			if (XR_FAILED(res))
				Con_Printf("xrWaitSwapchainImage: %s\n", XR_StringForResult(res));
			rendereye(&xr.eye[u].swapimages[imgidx], fovoverride, orientation);
			//GL note: the OpenXR specification says NOTHING about the application having to glFlush or glFinish.
			//	I take this to mean that the openxr runtime is responsible for setting up barriers or w/e inside ReleaseSwapchainImage.
			//VK note: the OpenXR spec does say that it needs to be color_attachment_optimal+owned by queue. which it is.
			//	I take this to mean that the openxr runtime is responsible for barriers (as it'll need to transition it to general or shader-read anyway).
			res = xrReleaseSwapchainImage(xr.eye[u].swapchain, NULL);
			if (XR_FAILED(res))
				Con_Printf("xrReleaseSwapchainImage: %s\n", XR_StringForResult(res));
		}
		proj.viewCount = u;
	}

	endframeinfo.layers = projlist;
	endframeinfo.displayTime = xr.framestate.predictedDisplayTime;
	endframeinfo.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;	//we don't do the alpha channel very well.
	res = xrEndFrame(xr.session, &endframeinfo);
	if (XR_FAILED(res))
	{
		Con_Printf("xrEndFrame: %s\n", XR_StringForResult(res));
		if (res == XR_ERROR_SESSION_LOST || res == XR_ERROR_SESSION_NOT_RUNNING || res == XR_ERROR_SWAPCHAIN_RECT_INVALID)
			XR_SessionEnded();	//something sessiony
		else //if (res == XR_ERROR_INSTANCE_LOST)
			XR_Shutdown();	//don't really know what it was, just kill everything
	}

	return xr_skipregularview->ival;
}

static plugvrfuncs_t openxr =
{
	"OpenXR",
	XR_PreInit,
	XR_Init,
	XR_SyncFrame,
	XR_Render,
	XR_Shutdown,
};

qboolean Plug_Init(void)
{
#ifdef XR_NO_PROTOTYPES
	{
		static dllhandle_t *lib;
		static dllfunction_t funcs[] = {
			#define XRFUNC(n) {(void*)&n, #n},
				XRFUNCS
			#undef XRFUNC
			{NULL}};
#ifdef _WIN32
	#define XR_LOADER_LIBNAME "openxr_loader"ARCH_DL_POSTFIX
#else
	#define XR_LOADER_LIBNAME "libopenxr_loader"ARCH_DL_POSTFIX".1"
#endif
		if (!lib)
			lib = plugfuncs->LoadDLL(XR_LOADER_LIBNAME, funcs);
		if (!lib)
		{
			Con_Printf(CON_ERROR"OpenXR: Unable to load "XR_LOADER_LIBNAME"\n");
			return false;
		}
	}
#endif

	fsfuncs = plugfuncs->GetEngineInterface(plugfsfuncs_name, sizeof(*fsfuncs));
	inputfuncs = plugfuncs->GetEngineInterface(pluginputfuncs_name, sizeof(*inputfuncs));
	plugfuncs->ExportFunction("MayUnload", XR_PluginMayUnload);
	plugfuncs->ExportFunction("ExecuteCommand", XR_HapticCommand_f);
	if (plugfuncs->ExportInterface(plugvrfuncs_name, &openxr, sizeof(openxr)))
	{
		xr_enable			= cvarfuncs->GetNVFDG("xr_enable",			"1",			0,				"Controls whether to use openxr rendering or not.",									"OpenXR configuration");
		xr_debug			= cvarfuncs->GetNVFDG("xr_debug",			"0",			0,				"Controls whether to spam debug info or not.",										"OpenXR configuration");
		xr_formfactor		= cvarfuncs->GetNVFDG("xr_formfactor",		"head",			CVAR_ARCHIVE,	"Controls which VR system to try to use. Valid options are head, or hand",			"OpenXR configuration");
		xr_viewconfig		= cvarfuncs->GetNVFDG("xr_viewconfig",		"",				CVAR_ARCHIVE,	"Controls the type of view we aim for. Valid options are mono, stereo, or quad",	"OpenXR configuration");
		xr_metresize		= cvarfuncs->GetNVFDG("xr_metresize",		"26.24671916",	CVAR_ARCHIVE,	"Size of a metre in game units",													"OpenXR configuration");
		xr_skipregularview	= cvarfuncs->GetNVFDG("xr_skipregularview", "1",			CVAR_ARCHIVE,	"Skip rendering the regular view when OpenXR is active.",							"OpenXR configuration");
		xr_fingertracking	= cvarfuncs->GetNVFDG("xr_fingertracking",	"0",			0,	"Attempt to track individual finger joints.",													"OpenXR configuration");
		return true;
	}
	return false;
}
