quakequest/Projects/Android/jni/QuakeQuestSrc/OpenXrInput_MetaQuest.c
Simon b5e9618d81 Several Fixes
- Allow super sampling
- Fixes to haptic feedback
- Default brightness on Pico to 1.4
2023-01-15 15:44:20 +00:00

502 lines
23 KiB
C

#ifdef META_QUEST
#include "VrCommon.h"
extern ovrApp gAppState;
XrSpace CreateActionSpace(XrAction poseAction, XrPath subactionPath) {
XrActionSpaceCreateInfo asci = {};
asci.type = XR_TYPE_ACTION_SPACE_CREATE_INFO;
asci.action = poseAction;
asci.poseInActionSpace.orientation.w = 1.0f;
asci.subactionPath = subactionPath;
XrSpace actionSpace = XR_NULL_HANDLE;
OXR(xrCreateActionSpace(gAppState.Session, &asci, &actionSpace));
return actionSpace;
}
XrActionSuggestedBinding ActionSuggestedBinding(XrAction action, const char* bindingString) {
XrActionSuggestedBinding asb;
asb.action = action;
XrPath bindingPath;
OXR(xrStringToPath(gAppState.Instance, bindingString, &bindingPath));
asb.binding = bindingPath;
return asb;
}
XrActionSet CreateActionSet(int priority, const char* name, const char* localizedName) {
XrActionSetCreateInfo asci = {};
asci.type = XR_TYPE_ACTION_SET_CREATE_INFO;
asci.next = NULL;
asci.priority = priority;
strcpy(asci.actionSetName, name);
strcpy(asci.localizedActionSetName, localizedName);
XrActionSet actionSet = XR_NULL_HANDLE;
OXR(xrCreateActionSet(gAppState.Instance, &asci, &actionSet));
return actionSet;
}
XrAction CreateAction(
XrActionSet actionSet,
XrActionType type,
const char* actionName,
const char* localizedName,
int countSubactionPaths,
XrPath* subactionPaths) {
ALOGV("CreateAction %s, %" PRIi32, actionName, countSubactionPaths);
XrActionCreateInfo aci = {};
aci.type = XR_TYPE_ACTION_CREATE_INFO;
aci.next = NULL;
aci.actionType = type;
if (countSubactionPaths > 0) {
aci.countSubactionPaths = countSubactionPaths;
aci.subactionPaths = subactionPaths;
}
strcpy(aci.actionName, actionName);
strcpy(aci.localizedActionName, localizedName ? localizedName : actionName);
XrAction action = XR_NULL_HANDLE;
OXR(xrCreateAction(actionSet, &aci, &action));
return action;
}
bool ActionPoseIsActive(XrAction action, XrPath subactionPath) {
XrActionStateGetInfo getInfo = {};
getInfo.type = XR_TYPE_ACTION_STATE_GET_INFO;
getInfo.action = action;
getInfo.subactionPath = subactionPath;
XrActionStatePose state = {};
state.type = XR_TYPE_ACTION_STATE_POSE;
OXR(xrGetActionStatePose(gAppState.Session, &getInfo, &state));
return state.isActive != XR_FALSE;
}
XrActionStateFloat GetActionStateFloat(XrAction action) {
XrActionStateGetInfo getInfo = {};
getInfo.type = XR_TYPE_ACTION_STATE_GET_INFO;
getInfo.action = action;
XrActionStateFloat state = {};
state.type = XR_TYPE_ACTION_STATE_FLOAT;
OXR(xrGetActionStateFloat(gAppState.Session, &getInfo, &state));
return state;
}
XrActionStateBoolean GetActionStateBoolean(XrAction action) {
XrActionStateGetInfo getInfo = {};
getInfo.type = XR_TYPE_ACTION_STATE_GET_INFO;
getInfo.action = action;
XrActionStateBoolean state = {};
state.type = XR_TYPE_ACTION_STATE_BOOLEAN;
OXR(xrGetActionStateBoolean(gAppState.Session, &getInfo, &state));
return state;
}
XrActionStateVector2f GetActionStateVector2(XrAction action) {
XrActionStateGetInfo getInfo = {};
getInfo.type = XR_TYPE_ACTION_STATE_GET_INFO;
getInfo.action = action;
XrActionStateVector2f state = {};
state.type = XR_TYPE_ACTION_STATE_VECTOR2F;
OXR(xrGetActionStateVector2f(gAppState.Session, &getInfo, &state));
return state;
}
//OpenXR
XrPath leftHandPath;
XrPath rightHandPath;
XrAction handPoseLeftAction;
XrAction handPoseRightAction;
XrAction indexLeftAction;
XrAction indexRightAction;
XrAction menuAction;
XrAction buttonAAction;
XrAction buttonBAction;
XrAction buttonXAction;
XrAction buttonYAction;
XrAction gripLeftAction;
XrAction gripRightAction;
XrAction moveOnLeftJoystickAction;
XrAction moveOnRightJoystickAction;
XrAction thumbstickLeftClickAction;
XrAction thumbstickRightClickAction;
XrAction vibrateLeftFeedback;
XrAction vibrateRightFeedback;
XrActionSet runningActionSet;
XrSpace leftControllerAimSpace = XR_NULL_HANDLE;
XrSpace rightControllerAimSpace = XR_NULL_HANDLE;
bool inputInitialized = false;
bool useSimpleProfile = false;
void TBXR_InitActions( void )
{
// Actions
runningActionSet = CreateActionSet(1, "running_action_set", "Action Set used on main loop");
indexLeftAction = CreateAction(runningActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "index_left", "Index left", 0, NULL);
indexRightAction = CreateAction(runningActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "index_right", "Index right", 0, NULL);
menuAction = CreateAction(runningActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "menu_action", "Menu", 0, NULL);
buttonAAction = CreateAction(runningActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "button_a", "Button A", 0, NULL);
buttonBAction = CreateAction(runningActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "button_b", "Button B", 0, NULL);
buttonXAction = CreateAction(runningActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "button_x", "Button X", 0, NULL);
buttonYAction = CreateAction(runningActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "button_y", "Button Y", 0, NULL);
gripLeftAction = CreateAction(runningActionSet, XR_ACTION_TYPE_FLOAT_INPUT, "grip_left", "Grip left", 0, NULL);
gripRightAction = CreateAction(runningActionSet, XR_ACTION_TYPE_FLOAT_INPUT, "grip_right", "Grip right", 0, NULL);
moveOnLeftJoystickAction = CreateAction(runningActionSet, XR_ACTION_TYPE_VECTOR2F_INPUT, "move_on_left_joy", "Move on left Joy", 0, NULL);
moveOnRightJoystickAction = CreateAction(runningActionSet, XR_ACTION_TYPE_VECTOR2F_INPUT, "move_on_right_joy", "Move on right Joy", 0, NULL);
thumbstickLeftClickAction = CreateAction(runningActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "thumbstick_left", "Thumbstick left", 0, NULL);
thumbstickRightClickAction = CreateAction(runningActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "thumbstick_right", "Thumbstick right", 0, NULL);
vibrateLeftFeedback = CreateAction(runningActionSet, XR_ACTION_TYPE_VIBRATION_OUTPUT, "vibrate_left_feedback", "Vibrate Left Controller Feedback", 0, NULL);
vibrateRightFeedback = CreateAction(runningActionSet, XR_ACTION_TYPE_VIBRATION_OUTPUT, "vibrate_right_feedback", "Vibrate Right Controller Feedback", 0, NULL);
OXR(xrStringToPath(gAppState.Instance, "/user/hand/left", &leftHandPath));
OXR(xrStringToPath(gAppState.Instance, "/user/hand/right", &rightHandPath));
handPoseLeftAction = CreateAction(runningActionSet, XR_ACTION_TYPE_POSE_INPUT, "hand_pose_left", NULL, 1, &leftHandPath);
handPoseRightAction = CreateAction(runningActionSet, XR_ACTION_TYPE_POSE_INPUT, "hand_pose_right", NULL, 1, &rightHandPath);
if (leftControllerAimSpace == XR_NULL_HANDLE) {
leftControllerAimSpace = CreateActionSpace(handPoseLeftAction, leftHandPath);
}
if (rightControllerAimSpace == XR_NULL_HANDLE) {
rightControllerAimSpace = CreateActionSpace(handPoseRightAction, rightHandPath);
}
XrPath interactionProfilePath = XR_NULL_PATH;
XrPath interactionProfilePathTouch = XR_NULL_PATH;
XrPath interactionProfilePathKHRSimple = XR_NULL_PATH;
OXR(xrStringToPath(gAppState.Instance, "/interaction_profiles/oculus/touch_controller", &interactionProfilePathTouch));
OXR(xrStringToPath(gAppState.Instance, "/interaction_profiles/khr/simple_controller", &interactionProfilePathKHRSimple));
// Toggle this to force simple as a first choice, otherwise use it as a last resort
if (useSimpleProfile) {
ALOGV("xrSuggestInteractionProfileBindings found bindings for Khronos SIMPLE controller");
interactionProfilePath = interactionProfilePathKHRSimple;
} else {
// Query Set
XrActionSet queryActionSet = CreateActionSet(1, "query_action_set", "Action Set used to query device caps");
XrAction dummyAction = CreateAction(queryActionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "dummy_action", "Dummy Action", 0, NULL);
// Map bindings
XrActionSuggestedBinding bindings[1];
int currBinding = 0;
bindings[currBinding++] = ActionSuggestedBinding(dummyAction, "/user/hand/right/input/system/click");
XrInteractionProfileSuggestedBinding suggestedBindings = {};
suggestedBindings.type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING;
suggestedBindings.next = NULL;
suggestedBindings.suggestedBindings = bindings;
suggestedBindings.countSuggestedBindings = currBinding;
// Try all
suggestedBindings.interactionProfile = interactionProfilePathTouch;
XrResult suggestTouchResult = xrSuggestInteractionProfileBindings(gAppState.Instance, &suggestedBindings);
OXR(suggestTouchResult);
if (XR_SUCCESS == suggestTouchResult) {
ALOGV("xrSuggestInteractionProfileBindings found bindings for QUEST controller");
interactionProfilePath = interactionProfilePathTouch;
}
if (interactionProfilePath == XR_NULL_PATH) {
// Simple as a fallback
bindings[0] = ActionSuggestedBinding(dummyAction, "/user/hand/right/input/select/click");
suggestedBindings.interactionProfile = interactionProfilePathKHRSimple;
XrResult suggestKHRSimpleResult = xrSuggestInteractionProfileBindings(gAppState.Instance, &suggestedBindings);
OXR(suggestKHRSimpleResult);
if (XR_SUCCESS == suggestKHRSimpleResult) {
ALOGV("xrSuggestInteractionProfileBindings found bindings for Khronos SIMPLE controller");
interactionProfilePath = interactionProfilePathKHRSimple;
} else {
ALOGE("xrSuggestInteractionProfileBindings did NOT find any bindings.");
assert(false);
}
}
}
// Action creation
{
// Map bindings
XrActionSuggestedBinding bindings[32]; // large enough for all profiles
int currBinding = 0;
{
if (interactionProfilePath == interactionProfilePathTouch) {
bindings[currBinding++] = ActionSuggestedBinding(indexLeftAction, "/user/hand/left/input/trigger");
bindings[currBinding++] = ActionSuggestedBinding(indexRightAction, "/user/hand/right/input/trigger");
bindings[currBinding++] = ActionSuggestedBinding(menuAction, "/user/hand/left/input/menu/click");
bindings[currBinding++] = ActionSuggestedBinding(buttonXAction, "/user/hand/left/input/x/click");
bindings[currBinding++] = ActionSuggestedBinding(buttonYAction, "/user/hand/left/input/y/click");
bindings[currBinding++] = ActionSuggestedBinding(buttonAAction, "/user/hand/right/input/a/click");
bindings[currBinding++] = ActionSuggestedBinding(buttonBAction, "/user/hand/right/input/b/click");
bindings[currBinding++] = ActionSuggestedBinding(gripLeftAction, "/user/hand/left/input/squeeze/value");
bindings[currBinding++] = ActionSuggestedBinding(gripRightAction, "/user/hand/right/input/squeeze/value");
bindings[currBinding++] = ActionSuggestedBinding(moveOnLeftJoystickAction, "/user/hand/left/input/thumbstick");
bindings[currBinding++] = ActionSuggestedBinding(moveOnRightJoystickAction, "/user/hand/right/input/thumbstick");
bindings[currBinding++] = ActionSuggestedBinding(thumbstickLeftClickAction, "/user/hand/left/input/thumbstick/click");
bindings[currBinding++] = ActionSuggestedBinding(thumbstickRightClickAction, "/user/hand/right/input/thumbstick/click");
bindings[currBinding++] = ActionSuggestedBinding(vibrateLeftFeedback, "/user/hand/left/output/haptic");
bindings[currBinding++] = ActionSuggestedBinding(vibrateRightFeedback, "/user/hand/right/output/haptic");
bindings[currBinding++] = ActionSuggestedBinding(handPoseLeftAction, "/user/hand/left/input/aim/pose");
bindings[currBinding++] = ActionSuggestedBinding(handPoseRightAction, "/user/hand/right/input/aim/pose");
}
if (interactionProfilePath == interactionProfilePathKHRSimple) {
bindings[currBinding++] = ActionSuggestedBinding(indexLeftAction, "/user/hand/left/input/select/click");
bindings[currBinding++] = ActionSuggestedBinding(indexRightAction, "/user/hand/right/input/select/click");
bindings[currBinding++] = ActionSuggestedBinding(buttonAAction, "/user/hand/left/input/menu/click");
bindings[currBinding++] = ActionSuggestedBinding(buttonXAction, "/user/hand/right/input/menu/click");
bindings[currBinding++] = ActionSuggestedBinding(vibrateLeftFeedback, "/user/hand/left/output/haptic");
bindings[currBinding++] = ActionSuggestedBinding(vibrateRightFeedback, "/user/hand/right/output/haptic");
bindings[currBinding++] = ActionSuggestedBinding(handPoseLeftAction, "/user/hand/left/input/aim/pose");
bindings[currBinding++] = ActionSuggestedBinding(handPoseRightAction, "/user/hand/right/input/aim/pose");
}
}
XrInteractionProfileSuggestedBinding suggestedBindings = {};
suggestedBindings.type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING;
suggestedBindings.next = NULL;
suggestedBindings.interactionProfile = interactionProfilePath;
suggestedBindings.suggestedBindings = bindings;
suggestedBindings.countSuggestedBindings = currBinding;
OXR(xrSuggestInteractionProfileBindings(gAppState.Instance, &suggestedBindings));
// Enumerate actions
XrPath actionPathsBuffer[32];
char stringBuffer[256];
XrAction actionsToEnumerate[] = {
indexLeftAction,
indexRightAction,
menuAction,
buttonAAction,
buttonBAction,
buttonXAction,
buttonYAction,
gripLeftAction,
gripRightAction,
moveOnLeftJoystickAction,
moveOnRightJoystickAction,
thumbstickLeftClickAction,
thumbstickRightClickAction,
vibrateLeftFeedback,
vibrateRightFeedback,
handPoseLeftAction,
handPoseRightAction
};
for (size_t i = 0; i < sizeof(actionsToEnumerate) / sizeof(actionsToEnumerate[0]); ++i) {
XrBoundSourcesForActionEnumerateInfo enumerateInfo = {};
enumerateInfo.type = XR_TYPE_BOUND_SOURCES_FOR_ACTION_ENUMERATE_INFO;
enumerateInfo.next = NULL;
enumerateInfo.action = actionsToEnumerate[i];
// Get Count
uint32_t countOutput = 0;
OXR(xrEnumerateBoundSourcesForAction(
gAppState.Session, &enumerateInfo, 0 /* request size */, &countOutput, NULL));
ALOGV(
"xrEnumerateBoundSourcesForAction action=%lld count=%u",
(long long)enumerateInfo.action,
countOutput);
if (countOutput < 32) {
OXR(xrEnumerateBoundSourcesForAction(
gAppState.Session, &enumerateInfo, 32, &countOutput, actionPathsBuffer));
for (uint32_t a = 0; a < countOutput; ++a) {
XrInputSourceLocalizedNameGetInfo nameGetInfo = {};
nameGetInfo.type = XR_TYPE_INPUT_SOURCE_LOCALIZED_NAME_GET_INFO;
nameGetInfo.next = NULL;
nameGetInfo.sourcePath = actionPathsBuffer[a];
nameGetInfo.whichComponents = XR_INPUT_SOURCE_LOCALIZED_NAME_USER_PATH_BIT |
XR_INPUT_SOURCE_LOCALIZED_NAME_INTERACTION_PROFILE_BIT |
XR_INPUT_SOURCE_LOCALIZED_NAME_COMPONENT_BIT;
uint32_t stringCount = 0u;
OXR(xrGetInputSourceLocalizedName(
gAppState.Session, &nameGetInfo, 0, &stringCount, NULL));
if (stringCount < 256) {
OXR(xrGetInputSourceLocalizedName(
gAppState.Session, &nameGetInfo, 256, &stringCount, stringBuffer));
char pathStr[256];
uint32_t strLen = 0;
OXR(xrPathToString(
gAppState.Instance,
actionPathsBuffer[a],
(uint32_t)sizeof(pathStr),
&strLen,
pathStr));
ALOGV(
" -> path = %lld `%s` -> `%s`",
(long long)actionPathsBuffer[a],
pathStr,
stringBuffer);
}
}
}
}
}
// Attach to session
XrSessionActionSetsAttachInfo attachInfo = {};
attachInfo.type = XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO;
attachInfo.next = NULL;
attachInfo.countActionSets = 1;
attachInfo.actionSets = &runningActionSet;
OXR(xrAttachSessionActionSets(gAppState.Session, &attachInfo));
inputInitialized = true;
}
void TBXR_SyncActions( void )
{
// sync action data
XrActiveActionSet activeActionSet = {};
activeActionSet.actionSet = runningActionSet;
activeActionSet.subactionPath = XR_NULL_PATH;
XrActionsSyncInfo syncInfo = {};
syncInfo.type = XR_TYPE_ACTIONS_SYNC_INFO;
syncInfo.next = NULL;
syncInfo.countActiveActionSets = 1;
syncInfo.activeActionSets = &activeActionSet;
OXR(xrSyncActions(gAppState.Session, &syncInfo));
// query input action states
XrActionStateGetInfo getInfo = {};
getInfo.type = XR_TYPE_ACTION_STATE_GET_INFO;
getInfo.next = NULL;
getInfo.subactionPath = XR_NULL_PATH;
}
void TBXR_UpdateControllers( )
{
TBXR_SyncActions();
//get controller poses
XrAction controller[] = {handPoseLeftAction, handPoseRightAction};
XrPath subactionPath[] = {leftHandPath, rightHandPath};
XrSpace controllerSpace[] = {leftControllerAimSpace, rightControllerAimSpace};
for (int i = 0; i < 2; i++) {
if (ActionPoseIsActive(controller[i], subactionPath[i])) {
XrSpaceVelocity vel = {};
vel.type = XR_TYPE_SPACE_VELOCITY;
XrSpaceLocation loc = {};
loc.type = XR_TYPE_SPACE_LOCATION;
loc.next = &vel;
OXR(xrLocateSpace(controllerSpace[i], gAppState.CurrentSpace, gAppState.PredictedDisplayTime, &loc));
gAppState.TrackedController[i].Active = (loc.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0;
gAppState.TrackedController[i].Pose = loc.pose;
gAppState.TrackedController[i].Velocity = vel;
} else {
ovrTrackedController_Clear(&gAppState.TrackedController[i]);
}
}
leftRemoteTracking_new = gAppState.TrackedController[0];
rightRemoteTracking_new = gAppState.TrackedController[1];
memset(&leftTrackedRemoteState_new, 0, sizeof leftTrackedRemoteState_new);
memset(&rightTrackedRemoteState_new, 0, sizeof rightTrackedRemoteState_new);
//button mapping
if (GetActionStateBoolean(menuAction).currentState) leftTrackedRemoteState_new.Buttons |= xrButton_Enter;
if (GetActionStateBoolean(buttonXAction).currentState) leftTrackedRemoteState_new.Buttons |= xrButton_X;
if (GetActionStateBoolean(buttonYAction).currentState) leftTrackedRemoteState_new.Buttons |= xrButton_Y;
leftTrackedRemoteState_new.GripTrigger = GetActionStateFloat(gripLeftAction).currentState;
if (leftTrackedRemoteState_new.GripTrigger > 0.5f) leftTrackedRemoteState_new.Buttons |= xrButton_GripTrigger;
if (GetActionStateBoolean(thumbstickLeftClickAction).currentState) leftTrackedRemoteState_new.Buttons |= xrButton_LThumb;
if (GetActionStateBoolean(buttonAAction).currentState) rightTrackedRemoteState_new.Buttons |= xrButton_A;
if (GetActionStateBoolean(buttonBAction).currentState) rightTrackedRemoteState_new.Buttons |= xrButton_B;
rightTrackedRemoteState_new.GripTrigger = GetActionStateFloat(gripRightAction).currentState;
if (rightTrackedRemoteState_new.GripTrigger > 0.5f) rightTrackedRemoteState_new.Buttons |= xrButton_GripTrigger;
if (GetActionStateBoolean(thumbstickRightClickAction).currentState) rightTrackedRemoteState_new.Buttons |= xrButton_RThumb;
//index finger click
if (GetActionStateBoolean(indexLeftAction).currentState) leftTrackedRemoteState_new.Buttons |= xrButton_Trigger;
if (GetActionStateBoolean(indexRightAction).currentState) rightTrackedRemoteState_new.Buttons |= xrButton_Trigger;
//thumbstick
XrActionStateVector2f moveJoystickState;
moveJoystickState = GetActionStateVector2(moveOnLeftJoystickAction);
leftTrackedRemoteState_new.Joystick.x = moveJoystickState.currentState.x;
leftTrackedRemoteState_new.Joystick.y = moveJoystickState.currentState.y;
moveJoystickState = GetActionStateVector2(moveOnRightJoystickAction);
rightTrackedRemoteState_new.Joystick.x = moveJoystickState.currentState.x;
rightTrackedRemoteState_new.Joystick.y = moveJoystickState.currentState.y;
}
//0 = left, 1 = right
float vibration_channel_duration[2] = {0.0f, 0.0f};
float vibration_channel_intensity[2] = {0.0f, 0.0f};
void TBXR_Vibrate( int duration, int chan, float intensity )
{
for (int i = 0; i < 2; ++i)
{
if ((i + 1) & chan)
{
if (vibration_channel_duration[i] > 0.0f)
return;
if (vibration_channel_duration[i] == -1.0f && duration != 0.0f)
return;
vibration_channel_duration[i] = duration;
vibration_channel_intensity[i] = intensity;
}
}
}
void TBXR_ProcessHaptics() {
static float lastFrameTime = 0.0f;
float timestamp = (float)(TBXR_GetTimeInMilliSeconds());
float frametime = timestamp - lastFrameTime;
lastFrameTime = timestamp;
for (int i = 0; i < 2; ++i) {
if (vibration_channel_duration[i] > 0.0f ||
vibration_channel_duration[i] == -1.0f) {
// fire haptics using output action
XrHapticVibration vibration = {};
vibration.type = XR_TYPE_HAPTIC_VIBRATION;
vibration.next = NULL;
vibration.amplitude = vibration_channel_intensity[i];
vibration.duration = ToXrTime(vibration_channel_duration[i]);
vibration.frequency = 3000;
XrHapticActionInfo hapticActionInfo = {};
hapticActionInfo.type = XR_TYPE_HAPTIC_ACTION_INFO;
hapticActionInfo.next = NULL;
hapticActionInfo.action = i == 0 ? vibrateLeftFeedback : vibrateRightFeedback;
OXR(xrApplyHapticFeedback(gAppState.Session, &hapticActionInfo, (const XrHapticBaseHeader*)&vibration));
if (vibration_channel_duration[i] != -1.0f) {
vibration_channel_duration[i] -= frametime;
if (vibration_channel_duration[i] < 0.0f) {
vibration_channel_duration[i] = 0.0f;
vibration_channel_intensity[i] = 0.0f;
}
}
} else {
// Stop haptics
XrHapticActionInfo hapticActionInfo = {};
hapticActionInfo.type = XR_TYPE_HAPTIC_ACTION_INFO;
hapticActionInfo.next = NULL;
hapticActionInfo.action = i == 0 ? vibrateLeftFeedback : vibrateRightFeedback;
OXR(xrStopHapticFeedback(gAppState.Session, &hapticActionInfo));
}
}
}
#endif