- xr_input.c/h: an OpenXR action set (aim pose, trigger/select click, a menu-toggle button) plus ray/quad hit-testing, and a visible laser-pointer line drawn from the controller toward the hit point. - xr_menu.c/h + MenuOverlay.java: a second composition-layer quad, toggled by a controller button, showing an off-screen, never-attached Android View tree (one "Keyboard" button so far) driven by synthetic touch events computed from the laser ray's hit UV. - Migrate gl4es from a prebuilt binary baked into the Docker build-image to a vendored source snapshot (android/gl4es-src/) compiled from source at APK build time via CMake add_subdirectory(), patched through a new android/gl4es-patches/ (mirrors the existing engine-patches/ pattern). This was needed to track down and fix a bug hit while building the menu: gl4es's fixed-pipeline emulation (fpe.c) was unconditionally substituting its own shader onto the menu's draw call (fpe_ReleventState() always sets alphafunc to a nonzero sentinel, so its fpe_IsEmpty() check could never see the state as empty), making the menu quad show the game's own rendering instead of its own content. Fixed by routing the menu's blit through a real glBlitFramebuffer() call instead of a shader-based draw, which gl4es's fpe.c has nothing to intercept - documented in README.md's new "Debugging notes" section. - Renumber android/engine-patches/ to close the gap left by removing an unrelated diagnostic-only patch, and strip investigation-journal comments and dead diagnostic code (temporary tracing, env-var probes) left over from finding the bug above. - Restructure README.md into numbered sections, document every engine/gl4es patch, add Android Studio dev/testing-cycle instructions, and generalize Quest-specific wording to any OpenXR headset. Update NOTICE.txt to match (gl4es is now vendored/patched source, not a prebuilt binary; add the OpenXR-SDK loader).
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
#include "xr_input.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <android/log.h>
|
||||
|
||||
#include <GLES3/gl3.h>
|
||||
|
||||
#include "xr_menu.h"
|
||||
#include "xr_session.h"
|
||||
|
||||
#define TAG "QuestShock"
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
|
||||
|
||||
#define LEFT 0
|
||||
#define RIGHT 1
|
||||
|
||||
static XrInstance g_instance = XR_NULL_HANDLE;
|
||||
static XrSession g_session = XR_NULL_HANDLE;
|
||||
static XrActionSet g_action_set = XR_NULL_HANDLE;
|
||||
static XrAction g_aim_pose_action = XR_NULL_HANDLE;
|
||||
static XrAction g_select_click_action = XR_NULL_HANDLE;
|
||||
static XrAction g_menu_toggle_action = XR_NULL_HANDLE;
|
||||
static XrPath g_hand_path[2];
|
||||
static XrSpace g_aim_space[2] = {XR_NULL_HANDLE, XR_NULL_HANDLE};
|
||||
|
||||
// Edge-detection state, so touch dispatch and logging only fire on actual
|
||||
// state changes, not every frame.
|
||||
static bool g_prev_select[2] = {false, false};
|
||||
static bool g_prev_menu = false;
|
||||
static bool g_prev_hit[2] = {false, false};
|
||||
// Tracks which hand currently has an in-flight synthetic touch down on the
|
||||
// menu (so a later trigger-up edge only dispatches a matching touch-up if
|
||||
// a touch-down was actually sent for that hand).
|
||||
static bool g_menu_touch_active[2] = {false, false};
|
||||
|
||||
static GLuint g_reticle_program = 0;
|
||||
static GLint g_reticle_color_loc = -1;
|
||||
|
||||
static bool xr_check(XrResult result, const char *what) {
|
||||
if (XR_SUCCEEDED(result))
|
||||
return true;
|
||||
char resultString[XR_MAX_RESULT_STRING_SIZE] = {0};
|
||||
if (g_instance != XR_NULL_HANDLE)
|
||||
xrResultToString(g_instance, result, resultString);
|
||||
LOGE("XR: %s failed: %s (%d)", what, resultString[0] ? resultString : "?", (int)result);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Standard quaternion-vector rotation (v + 2*cross(q.xyz, cross(q.xyz, v) +
|
||||
// q.w*v)) - used to turn the aim pose's orientation into its local -Z
|
||||
// ("forward") axis, the ray direction per the OpenXR aim-pose convention.
|
||||
static void quat_rotate_vec(const XrQuaternionf *q, float vx, float vy, float vz, float *outx,
|
||||
float *outy, float *outz) {
|
||||
float cx = q->y * vz - q->z * vy + q->w * vx;
|
||||
float cy = q->z * vx - q->x * vz + q->w * vy;
|
||||
float cz = q->x * vy - q->y * vx + q->w * vz;
|
||||
*outx = vx + 2.0f * (q->y * cz - q->z * cy);
|
||||
*outy = vy + 2.0f * (q->z * cx - q->x * cz);
|
||||
*outz = vz + 2.0f * (q->x * cy - q->y * cx);
|
||||
}
|
||||
|
||||
static GLuint compile_shader(GLenum type, const char *src) {
|
||||
GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &src, NULL);
|
||||
glCompileShader(shader);
|
||||
GLint compiled = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (!compiled) {
|
||||
char log[512];
|
||||
glGetShaderInfoLog(shader, sizeof(log), NULL, log);
|
||||
LOGE("XR: reticle shader compile failed: %s", log);
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
// A standalone flat-color shader, independent of the engine's own
|
||||
// textureShaderProgram (OpenGL.cc is a separate, C++-only translation
|
||||
// unit, and shader state isn't shared across programs anyway) - position
|
||||
// is emitted directly in clip space, matching the same [-1,1] local quad
|
||||
// coordinates android_draw_surface_as_quad() (see android/engine-patches/
|
||||
// 11-android-openxr-present.patch) already uses for its own vertex
|
||||
// positions, so no view/projection matrix is needed here either.
|
||||
static const char *kVertexSrc = "attribute vec3 position;\n"
|
||||
"void main() { gl_Position = vec4(position, 1.0); }\n";
|
||||
static const char *kFragmentSrc = "precision mediump float;\n"
|
||||
"uniform vec4 color;\n"
|
||||
"void main() { gl_FragColor = color; }\n";
|
||||
|
||||
// Attribute 0 to match kVertexSrc's single "position" attribute - fine to
|
||||
// reuse the same numeric index the engine's own immediate-mode drawing
|
||||
// treats specially, since that's a per-program binding and this program is
|
||||
// never current at the same time as gl4es's immediate-mode emulation runs;
|
||||
// xr_input_sync_and_draw() disables the array again right after drawing,
|
||||
// the same discipline android_draw_surface_as_quad() already established.
|
||||
#define RETICLE_POSITION_LOC 0
|
||||
|
||||
static bool xr_input_init_reticle_program(void) {
|
||||
GLuint vs = compile_shader(GL_VERTEX_SHADER, kVertexSrc);
|
||||
GLuint fs = compile_shader(GL_FRAGMENT_SHADER, kFragmentSrc);
|
||||
g_reticle_program = glCreateProgram();
|
||||
glAttachShader(g_reticle_program, vs);
|
||||
glAttachShader(g_reticle_program, fs);
|
||||
glBindAttribLocation(g_reticle_program, RETICLE_POSITION_LOC, "position");
|
||||
glLinkProgram(g_reticle_program);
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(g_reticle_program, GL_LINK_STATUS, &linked);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
if (!linked) {
|
||||
char log[512];
|
||||
glGetProgramInfoLog(g_reticle_program, sizeof(log), NULL, log);
|
||||
LOGE("XR: reticle program link failed: %s", log);
|
||||
return false;
|
||||
}
|
||||
g_reticle_color_loc = glGetUniformLocation(g_reticle_program, "color");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool xr_input_init(XrInstance instance, XrSession session) {
|
||||
g_instance = instance;
|
||||
g_session = session;
|
||||
|
||||
xrStringToPath(instance, "/user/hand/left", &g_hand_path[LEFT]);
|
||||
xrStringToPath(instance, "/user/hand/right", &g_hand_path[RIGHT]);
|
||||
|
||||
XrActionSetCreateInfo setInfo = {XR_TYPE_ACTION_SET_CREATE_INFO};
|
||||
strncpy(setInfo.actionSetName, "gameplay", XR_MAX_ACTION_SET_NAME_SIZE - 1);
|
||||
strncpy(setInfo.localizedActionSetName, "Gameplay", XR_MAX_LOCALIZED_ACTION_SET_NAME_SIZE - 1);
|
||||
setInfo.priority = 0;
|
||||
if (!xr_check(xrCreateActionSet(instance, &setInfo, &g_action_set), "xrCreateActionSet"))
|
||||
return false;
|
||||
|
||||
XrActionCreateInfo aimInfo = {XR_TYPE_ACTION_CREATE_INFO};
|
||||
strncpy(aimInfo.actionName, "aim_pose", XR_MAX_ACTION_NAME_SIZE - 1);
|
||||
strncpy(aimInfo.localizedActionName, "Aim Pose", XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
|
||||
aimInfo.actionType = XR_ACTION_TYPE_POSE_INPUT;
|
||||
aimInfo.countSubactionPaths = 2;
|
||||
aimInfo.subactionPaths = g_hand_path;
|
||||
if (!xr_check(xrCreateAction(g_action_set, &aimInfo, &g_aim_pose_action),
|
||||
"xrCreateAction(aim_pose)"))
|
||||
return false;
|
||||
|
||||
XrActionCreateInfo selectInfo = {XR_TYPE_ACTION_CREATE_INFO};
|
||||
strncpy(selectInfo.actionName, "select_click", XR_MAX_ACTION_NAME_SIZE - 1);
|
||||
strncpy(selectInfo.localizedActionName, "Select", XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
|
||||
selectInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
|
||||
selectInfo.countSubactionPaths = 2;
|
||||
selectInfo.subactionPaths = g_hand_path;
|
||||
if (!xr_check(xrCreateAction(g_action_set, &selectInfo, &g_select_click_action),
|
||||
"xrCreateAction(select_click)"))
|
||||
return false;
|
||||
|
||||
// The touch_controller profile's "menu" component (the three-line
|
||||
// hamburger icon) only exists on the left controller - there's no
|
||||
// right-hand equivalent.
|
||||
XrActionCreateInfo menuInfo = {XR_TYPE_ACTION_CREATE_INFO};
|
||||
strncpy(menuInfo.actionName, "menu_toggle", XR_MAX_ACTION_NAME_SIZE - 1);
|
||||
strncpy(menuInfo.localizedActionName, "Menu Toggle", XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
|
||||
menuInfo.actionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
|
||||
menuInfo.countSubactionPaths = 1;
|
||||
menuInfo.subactionPaths = &g_hand_path[LEFT];
|
||||
if (!xr_check(xrCreateAction(g_action_set, &menuInfo, &g_menu_toggle_action),
|
||||
"xrCreateAction(menu_toggle)"))
|
||||
return false;
|
||||
|
||||
XrPath profilePath;
|
||||
xrStringToPath(instance, "/interaction_profiles/oculus/touch_controller", &profilePath);
|
||||
|
||||
// touch_controller has no .../trigger/click component, only the float
|
||||
// .../trigger/value - binding this boolean action straight to it
|
||||
// relies on OpenXR's standard action-type equivalence (runtimes
|
||||
// auto-convert float inputs to boolean via an internal threshold), the
|
||||
// same pattern Khronos' own hello_xr sample uses for its "select"
|
||||
// action. Suggesting a binding to a component the profile doesn't
|
||||
// have at all (like a nonexistent .../trigger/click) fails the entire
|
||||
// xrSuggestInteractionProfileBindings call, not just that one entry.
|
||||
XrPath aimPoseLeftPath, aimPoseRightPath, triggerLeftPath, triggerRightPath, menuClickPath;
|
||||
xrStringToPath(instance, "/user/hand/left/input/aim/pose", &aimPoseLeftPath);
|
||||
xrStringToPath(instance, "/user/hand/right/input/aim/pose", &aimPoseRightPath);
|
||||
xrStringToPath(instance, "/user/hand/left/input/trigger/value", &triggerLeftPath);
|
||||
xrStringToPath(instance, "/user/hand/right/input/trigger/value", &triggerRightPath);
|
||||
xrStringToPath(instance, "/user/hand/left/input/menu/click", &menuClickPath);
|
||||
|
||||
XrActionSuggestedBinding bindings[] = {
|
||||
{g_aim_pose_action, aimPoseLeftPath}, {g_aim_pose_action, aimPoseRightPath},
|
||||
{g_select_click_action, triggerLeftPath}, {g_select_click_action, triggerRightPath},
|
||||
{g_menu_toggle_action, menuClickPath},
|
||||
};
|
||||
XrInteractionProfileSuggestedBinding suggested = {XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
|
||||
suggested.interactionProfile = profilePath;
|
||||
suggested.countSuggestedBindings = sizeof(bindings) / sizeof(bindings[0]);
|
||||
suggested.suggestedBindings = bindings;
|
||||
if (!xr_check(xrSuggestInteractionProfileBindings(instance, &suggested),
|
||||
"xrSuggestInteractionProfileBindings"))
|
||||
return false;
|
||||
|
||||
for (int hand = 0; hand < 2; hand++) {
|
||||
XrActionSpaceCreateInfo spaceInfo = {XR_TYPE_ACTION_SPACE_CREATE_INFO};
|
||||
spaceInfo.action = g_aim_pose_action;
|
||||
spaceInfo.subactionPath = g_hand_path[hand];
|
||||
spaceInfo.poseInActionSpace.orientation.w = 1.0f;
|
||||
if (!xr_check(xrCreateActionSpace(session, &spaceInfo, &g_aim_space[hand]),
|
||||
"xrCreateActionSpace(aim)"))
|
||||
return false;
|
||||
}
|
||||
|
||||
XrSessionActionSetsAttachInfo attachInfo = {XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO};
|
||||
attachInfo.countActionSets = 1;
|
||||
attachInfo.actionSets = &g_action_set;
|
||||
if (!xr_check(xrAttachSessionActionSets(session, &attachInfo), "xrAttachSessionActionSets"))
|
||||
return false;
|
||||
|
||||
if (!xr_input_init_reticle_program())
|
||||
return false;
|
||||
|
||||
LOGI("XR: input action set ready (aim pose + trigger + menu-toggle)");
|
||||
return true;
|
||||
}
|
||||
|
||||
void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
||||
if (g_action_set == XR_NULL_HANDLE)
|
||||
return;
|
||||
|
||||
XrActiveActionSet activeSet = {g_action_set, XR_NULL_PATH};
|
||||
XrActionsSyncInfo syncInfo = {XR_TYPE_ACTIONS_SYNC_INFO};
|
||||
syncInfo.countActiveActionSets = 1;
|
||||
syncInfo.activeActionSets = &activeSet;
|
||||
xr_check(xrSyncActions(g_session, &syncInfo), "xrSyncActions");
|
||||
|
||||
bool menuVisible = xr_menu_is_visible();
|
||||
|
||||
float quadCenterX = 0.0f, quadCenterY = 0.0f, distance, halfWidth, halfHeight;
|
||||
if (menuVisible)
|
||||
xr_menu_get_quad_extent(&quadCenterX, &quadCenterY, &distance, &halfWidth, &halfHeight);
|
||||
else
|
||||
xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight);
|
||||
|
||||
static const char *kHandName[2] = {"left", "right"};
|
||||
static const float kHandColor[2][4] = {
|
||||
{0.2f, 1.0f, 1.0f, 1.0f}, // left: cyan
|
||||
{1.0f, 0.85f, 0.1f, 1.0f}, // right: amber
|
||||
};
|
||||
|
||||
// The reticle is drawn directly into whatever framebuffer is currently
|
||||
// bound - the game quad's swapchain image (see xr_frame_end(), which
|
||||
// calls this while that image is still bound). That only makes sense
|
||||
// while aiming at the game quad; the menu quad's content comes from
|
||||
// MenuOverlay's rendered Bitmap instead (xr_menu.c), uploaded on a
|
||||
// separate swapchain, so no reticle is drawn for it here.
|
||||
bool drawReticle = draw && !menuVisible;
|
||||
if (drawReticle)
|
||||
glUseProgram(g_reticle_program);
|
||||
|
||||
for (int hand = 0; hand < 2; hand++) {
|
||||
XrActionStateGetInfo selectInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
||||
selectInfo.action = g_select_click_action;
|
||||
selectInfo.subactionPath = g_hand_path[hand];
|
||||
XrActionStateBoolean selectState = {XR_TYPE_ACTION_STATE_BOOLEAN};
|
||||
xrGetActionStateBoolean(g_session, &selectInfo, &selectState);
|
||||
bool selectDown = selectState.isActive && selectState.currentState;
|
||||
bool selectDownEdge = selectDown && !g_prev_select[hand];
|
||||
bool selectUpEdge = !selectDown && g_prev_select[hand];
|
||||
if (selectDown != g_prev_select[hand]) {
|
||||
LOGI("XR: %s trigger %s", kHandName[hand], selectDown ? "DOWN" : "UP");
|
||||
g_prev_select[hand] = selectDown;
|
||||
}
|
||||
|
||||
if (hand == LEFT) {
|
||||
XrActionStateGetInfo menuInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
||||
menuInfo.action = g_menu_toggle_action;
|
||||
menuInfo.subactionPath = g_hand_path[LEFT];
|
||||
XrActionStateBoolean menuState = {XR_TYPE_ACTION_STATE_BOOLEAN};
|
||||
xrGetActionStateBoolean(g_session, &menuInfo, &menuState);
|
||||
bool menuDown = menuState.isActive && menuState.currentState;
|
||||
if (menuDown && !g_prev_menu) {
|
||||
LOGI("XR: menu-toggle button DOWN");
|
||||
xr_menu_toggle_visible();
|
||||
// Avoid a stale hit/touch-active carrying over from
|
||||
// whichever quad was being tested against before the
|
||||
// toggle - the next frame re-evaluates against the new
|
||||
// target from a clean state.
|
||||
g_prev_hit[LEFT] = g_prev_hit[RIGHT] = false;
|
||||
g_menu_touch_active[LEFT] = g_menu_touch_active[RIGHT] = false;
|
||||
} else if (!menuDown && g_prev_menu) {
|
||||
LOGI("XR: menu-toggle button UP");
|
||||
}
|
||||
g_prev_menu = menuDown;
|
||||
}
|
||||
|
||||
XrActionStateGetInfo poseInfo = {XR_TYPE_ACTION_STATE_GET_INFO};
|
||||
poseInfo.action = g_aim_pose_action;
|
||||
poseInfo.subactionPath = g_hand_path[hand];
|
||||
XrActionStatePose poseState = {XR_TYPE_ACTION_STATE_POSE};
|
||||
xrGetActionStatePose(g_session, &poseInfo, &poseState);
|
||||
if (!poseState.isActive)
|
||||
continue;
|
||||
|
||||
XrSpaceLocation location = {XR_TYPE_SPACE_LOCATION};
|
||||
if (!xr_check(xrLocateSpace(g_aim_space[hand], baseSpace, time, &location), "xrLocateSpace"))
|
||||
continue;
|
||||
const XrSpaceLocationFlags needed =
|
||||
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
|
||||
if ((location.locationFlags & needed) != needed)
|
||||
continue;
|
||||
|
||||
float fx, fy, fz;
|
||||
quat_rotate_vec(&location.pose.orientation, 0.0f, 0.0f, -1.0f, &fx, &fy, &fz);
|
||||
|
||||
// The quad's plane is z = -distance (facing the viewer along -Z,
|
||||
// see QUAD_DISTANCE_METERS/xr_menu.c's MENU_DISTANCE_METERS) - a
|
||||
// ray parallel to it (fz ~ 0) never crosses. cx/cy are the hit
|
||||
// point in the quad's own [-1,1] local space (the same space
|
||||
// android_draw_surface_as_quad() draws its own vertex positions
|
||||
// in); u/v are the equivalent 0..1, top-left-origin coordinates
|
||||
// MenuOverlay's pixel grid and the on-quad hit logs both use.
|
||||
bool hit = false;
|
||||
float u = 0.0f, v = 0.0f, cx = 0.0f, cy = 0.0f;
|
||||
if (fabsf(fz) > 1e-5f) {
|
||||
float t = (-distance - location.pose.position.z) / fz;
|
||||
if (t > 0.0f) {
|
||||
cx = (location.pose.position.x + t * fx - quadCenterX) / halfWidth;
|
||||
cy = (location.pose.position.y + t * fy - quadCenterY) / halfHeight;
|
||||
hit = fabsf(cx) <= 1.0f && fabsf(cy) <= 1.0f;
|
||||
u = (cx + 1.0f) * 0.5f;
|
||||
v = (1.0f - cy) * 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
if (menuVisible) {
|
||||
if (hit != g_prev_hit[hand]) {
|
||||
LOGI("XR: %s aim ray %s menu quad (u=%.2f v=%.2f)", kHandName[hand],
|
||||
hit ? "entered" : "left", u, v);
|
||||
g_prev_hit[hand] = hit;
|
||||
}
|
||||
if (selectDownEdge && hit) {
|
||||
xr_menu_touch(u, v, true);
|
||||
g_menu_touch_active[hand] = true;
|
||||
} else if (selectUpEdge && g_menu_touch_active[hand]) {
|
||||
xr_menu_touch(u, v, false);
|
||||
g_menu_touch_active[hand] = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hit != g_prev_hit[hand]) {
|
||||
LOGI("XR: %s aim ray %s game quad (u=%.2f v=%.2f)", kHandName[hand],
|
||||
hit ? "entered" : "left", u, v);
|
||||
g_prev_hit[hand] = hit;
|
||||
}
|
||||
|
||||
if (!hit || !drawReticle)
|
||||
continue;
|
||||
|
||||
const float kSize = 0.03f;
|
||||
const float verts[] = {
|
||||
cx - kSize, cy, 0.0f, cx + kSize, cy, 0.0f, cx, cy - kSize, 0.0f, cx, cy + kSize, 0.0f,
|
||||
};
|
||||
glUniform4fv(g_reticle_color_loc, 1, kHandColor[hand]);
|
||||
glEnableVertexAttribArray(RETICLE_POSITION_LOC);
|
||||
glVertexAttribPointer(RETICLE_POSITION_LOC, 3, GL_FLOAT, GL_FALSE, 0, verts);
|
||||
glDrawArrays(GL_LINES, 0, 4);
|
||||
glDisableVertexAttribArray(RETICLE_POSITION_LOC);
|
||||
}
|
||||
}
|
||||
|
||||
void xr_input_shutdown(void) {
|
||||
for (int hand = 0; hand < 2; hand++) {
|
||||
if (g_aim_space[hand] != XR_NULL_HANDLE)
|
||||
xrDestroySpace(g_aim_space[hand]);
|
||||
g_aim_space[hand] = XR_NULL_HANDLE;
|
||||
}
|
||||
if (g_action_set != XR_NULL_HANDLE)
|
||||
xrDestroyActionSet(g_action_set);
|
||||
g_action_set = XR_NULL_HANDLE;
|
||||
g_aim_pose_action = XR_NULL_HANDLE;
|
||||
g_select_click_action = XR_NULL_HANDLE;
|
||||
g_menu_toggle_action = XR_NULL_HANDLE;
|
||||
|
||||
if (g_reticle_program != 0)
|
||||
glDeleteProgram(g_reticle_program);
|
||||
g_reticle_program = 0;
|
||||
|
||||
g_instance = XR_NULL_HANDLE;
|
||||
g_session = XR_NULL_HANDLE;
|
||||
memset(g_prev_select, 0, sizeof(g_prev_select));
|
||||
memset(g_prev_hit, 0, sizeof(g_prev_hit));
|
||||
memset(g_menu_touch_active, 0, sizeof(g_menu_touch_active));
|
||||
g_prev_menu = false;
|
||||
}
|
||||
Reference in New Issue
Block a user