fix(input): derive hand validity every sync; seqlock the input snapshot

lvalid/rvalid were only ever set to 1, so a controller that went off or lost
tracking kept reporting a tracked pose frozen at its last position. Validity
is now recomputed each xrr_input_sync from xrLocateSpace (success AND both
ORIENTATION_VALID and POSITION_VALID), so lost tracking reports untracked.

The render thread writes the state while the game thread reads it; publish it
through the same cheap seqlock the view snapshot uses so a reader never pairs
one frame's pose with another frame's valid flag (or reads a torn pose).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel LynchandClaude Opus 5.5 committed 2026-09-22 22:44:34 -04:00
1 parent 1d64595266
commit 2088845a52
1 file changed
+64 -19
+64 -19
View File
@@ -21,12 +21,43 @@ static XrAction a_lpose, a_rpose;
static XrAction a_lhaptic, a_rhaptic;
static XrSpace s_lspace, s_rspace;
static struct {
typedef struct {
uint32_t buttons;
float ltrig, rtrig, lgrip, rgrip;
ovrpVector2f lstick, rstick;
XrPosef lpose, rpose; int lvalid, rvalid;
} s_in;
} InputState;
/* Published input snapshot. The render thread (xrr_input_sync, inside begin_frame under
* g_xrlock — single writer) publishes it; the GAME thread reads it (controller state,
* node validity, hand poses). Same SEQLOCK as the view snapshot in xr_runtime.c: even
* s_inSeq = stable, odd = writer mid-update; readers retry instead of taking a lock, so a
* reader never sees a pose from one frame paired with another frame's valid flag. */
static volatile unsigned s_inSeq;
static InputState s_in;
static void input_publish(const InputState *st) {
unsigned s = s_inSeq;
__atomic_store_n(&s_inSeq, s + 1, __ATOMIC_RELAXED); /* odd: write in progress */
__atomic_thread_fence(__ATOMIC_RELEASE); /* odd-mark visible before data */
s_in = *st;
__atomic_store_n(&s_inSeq, s + 2, __ATOMIC_RELEASE); /* even: stable */
}
static void input_snapshot(InputState *out) {
for (;;) {
unsigned s1 = __atomic_load_n(&s_inSeq, __ATOMIC_ACQUIRE);
if (s1 & 1u) continue; /* writer mid-update -> retry */
*out = s_in;
__atomic_thread_fence(__ATOMIC_ACQUIRE); /* data reads complete before re-check */
if (__atomic_load_n(&s_inSeq, __ATOMIC_RELAXED) == s1) return;
}
}
/* A hand pose is usable only if the runtime vouches for BOTH position and orientation
* this frame; anything less (controller off, out of tracking) reports untracked so the
* game hides the hand instead of freezing it at the last pose. */
#define POSE_VALID_BITS (XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XR_SPACE_LOCATION_POSITION_VALID_BIT)
static XrAction mk(XrActionType t, const char *n) {
XrActionCreateInfo ci = { XR_TYPE_ACTION_CREATE_INFO };
@@ -138,6 +169,8 @@ void xrr_input_sync(void) {
if (sr != XR_SUCCESS && dbg < 3) { dbg++; XRRLOG("xrSyncActions rc=%d", (int)sr); }
if (XR_FAILED(sr)) return;
InputState st;
input_snapshot(&st); /* start from the last published state (keeps last poses) */
uint32_t btn = 0;
if (bget(a_A)) btn |= OVRP_BTN_A;
if (bget(a_B)) btn |= OVRP_BTN_B;
@@ -146,20 +179,27 @@ void xrr_input_sync(void) {
if (bget(a_menu)) btn |= OVRP_BTN_START;
if (bget(a_lstickc)) btn |= OVRP_BTN_LTHUMB;
if (bget(a_rstickc)) btn |= OVRP_BTN_RTHUMB;
s_in.buttons = btn;
s_in.ltrig = fget(a_ltrig); s_in.rtrig = fget(a_rtrig);
s_in.lgrip = fget(a_lgrip); s_in.rgrip = fget(a_rgrip);
s_in.lstick = v2get(a_lstick); s_in.rstick = v2get(a_rstick);
st.buttons = btn;
st.ltrig = fget(a_ltrig); st.rtrig = fget(a_rtrig);
st.lgrip = fget(a_lgrip); st.rgrip = fget(a_rgrip);
st.lstick = v2get(a_lstick); st.rstick = v2get(a_rstick);
/* validity is re-derived EVERY sync, so lost tracking drops back to untracked */
XrSpaceLocation loc = { XR_TYPE_SPACE_LOCATION };
XrResult lr = xrLocateSpace(s_lspace, g_xr.appSpace, g_xr.frameState.predictedDisplayTime, &loc);
if (lr == XR_SUCCESS && (loc.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)) {
s_in.lpose = loc.pose; s_in.lvalid = 1;
}
st.lvalid = (lr == XR_SUCCESS && (loc.locationFlags & POSE_VALID_BITS) == POSE_VALID_BITS);
if (st.lvalid) st.lpose = loc.pose;
XrSpaceLocation rloc = { XR_TYPE_SPACE_LOCATION };
XrResult rr = xrLocateSpace(s_rspace, g_xr.appSpace, g_xr.frameState.predictedDisplayTime, &rloc);
if (rr == XR_SUCCESS && (rloc.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)) {
s_in.rpose = rloc.pose; s_in.rvalid = 1;
st.rvalid = (rr == XR_SUCCESS && (rloc.locationFlags & POSE_VALID_BITS) == POSE_VALID_BITS);
if (st.rvalid) st.rpose = rloc.pose;
input_publish(&st);
{ /* DIAG: log each hand's tracked<->untracked transition (capped) */
static int plv = -1, prv = -1, tl = 0;
if ((st.lvalid != plv || st.rvalid != prv) && tl++ < 40)
XRRLOG("input: hand valid L=%d (flags=0x%x) R=%d (flags=0x%x)",
st.lvalid, (unsigned)loc.locationFlags, st.rvalid, (unsigned)rloc.locationFlags);
plv = st.lvalid; prv = st.rvalid;
}
static int pd = 0;
if (pd < 4) { pd++;
@@ -190,12 +230,13 @@ void xrr_set_vibration(unsigned int mask, float frequency, float amplitude) {
void xrr_get_controller_state(unsigned int mask, ovrpControllerState4 *out) {
(void)mask;
InputState st; input_snapshot(&st);
memset(out, 0, sizeof(*out));
out->ConnectedControllers = 0x01 | 0x02 | 0x80000000u; /* LTouch|RTouch|Active */
out->Buttons = s_in.buttons;
out->LIndexTrigger = s_in.ltrig; out->RIndexTrigger = s_in.rtrig;
out->LHandTrigger = s_in.lgrip; out->RHandTrigger = s_in.rgrip;
out->LThumbstick = s_in.lstick; out->RThumbstick = s_in.rstick;
out->Buttons = st.buttons;
out->LIndexTrigger = st.ltrig; out->RIndexTrigger = st.rtrig;
out->LHandTrigger = st.lgrip; out->RHandTrigger = st.rgrip;
out->LThumbstick = st.lstick; out->RThumbstick = st.rstick;
}
/* node presence/validity — the game gates pose queries on these. Reporting the
@@ -213,16 +254,20 @@ int xrr_node_valid(int node) {
case ovrpNode_Head: case ovrpNode_EyeLeft: case ovrpNode_EyeRight:
case ovrpNode_EyeCenter:
return g_xr.running ? 1 : 0;
case ovrpNode_HandLeft: return s_in.lvalid;
case ovrpNode_HandRight: return s_in.rvalid;
case ovrpNode_HandLeft:
case ovrpNode_HandRight: {
InputState st; input_snapshot(&st);
return node == ovrpNode_HandLeft ? st.lvalid : st.rvalid;
}
default: return 0;
}
}
int xrr_get_hand_pose(int node, ovrpPoseStatef *out) {
InputState st; input_snapshot(&st); /* pose + valid from the SAME sync */
const XrPosef *p = NULL;
if (node == ovrpNode_HandLeft && s_in.lvalid) p = &s_in.lpose;
if (node == ovrpNode_HandRight && s_in.rvalid) p = &s_in.rpose;
if (node == ovrpNode_HandLeft && st.lvalid) p = &st.lpose;
if (node == ovrpNode_HandRight && st.rvalid) p = &st.rpose;
if (!p) return 0;
ovrp_pose_from_xr(p, &out->Pose);
return 1;