Files
daveyandCraig 0e3ced6f65 vr: follow the headset through GET_VIDEO_VIEWS_STATUS
A core learns about a headset only from the HMD status bit;
SET_VIDEO_VIEWS returning true means the map was accepted. The scene
walker now enters VR on the first frame with HMD, sends a TRACKED map
at the frontend's recommended eye size and goes flat with an empty map.

A video reinit restarts the headset session, and entering or leaving VR
changes the geometry, which reinits video. A lost headset is waited for
five seconds, repeating the last frame, before the core goes flat.
Input is still polled on those frames.

While VR is tracking, the map is sent every retro_run(), as libretro.h
asks: it covers state loads and run-ahead. The frontend writes the
recommended eye size back on each call, so a new size there is taken as
a resize, and the AV info is sent once for it. A frontend that reports
a headset but refuses the tracked map is not asked again until the
headset drops or the VR option is turned off.

Co-authored-by: Craig <[email protected]>
2026-10-12 05:25:22 -07:00

115 lines
3.1 KiB
C++

/*
* Libretro 3DEngine - VR helpers
*
*/
#ifndef VR_SUPPORT_HPP__
#define VR_SUPPORT_HPP__
#include <math.h>
#include <string.h>
#include "libretro.h"
#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"
#include "glm/gtc/quaternion.hpp"
/* Identity pose + 90 degree symmetric FOV. Used until the frontend gives us
* a valid sample (first frame), so we never normalize a zero quaternion. */
static inline void vr_frame_defaults(struct retro_vr_frame_state *fs)
{
unsigned i, j;
memset(fs, 0, sizeof(*fs));
for (i = 0; i < 2; i++)
{
fs->eyes[i].orientation[3] = 1.0f;
for (j = 0; j < 4; j++)
fs->eyes[i].fov_tan[j] = 1.0f;
}
}
static inline glm::quat vr_quat(const float q[4])
{
/* libretro: (x, y, z, w). glm::quat constructor: (w, x, y, z). */
return glm::normalize(glm::quat(q[3], q[0], q[1], q[2]));
}
/* Eye -> tracking space transform. View matrix is the inverse of this
* (after applying the rig transform). */
static inline glm::mat4 vr_eye_pose_matrix(const struct retro_vr_eye_state &e)
{
glm::mat4 m = glm::mat4_cast(vr_quat(e.orientation));
m[3] = glm::vec4(e.position[0], e.position[1], e.position[2], 1.0f);
return m;
}
/* Asymmetric frustum from the four tangents. fabsf() so it works whether the
* frontend reports magnitudes or OpenXR-style signed angles
* (left/down negative) - see notes on fov_tan in the reply. */
static inline glm::mat4 vr_projection(const float t[4], float znear, float zfar)
{
float l = fabsf(t[0]) * znear;
float r = fabsf(t[1]) * znear;
float u = fabsf(t[2]) * znear;
float d = fabsf(t[3]) * znear;
return glm::frustum(-l, r, -d, u, znear, zfar);
}
/* Unit vector, in tracking space, of where the head is facing, flattened onto
* the ground plane. Averages both eyes (some HMDs cant their displays). */
static inline glm::vec3 vr_head_forward_xz(const struct retro_vr_frame_state &fs)
{
unsigned i;
glm::vec3 f(0.0f);
for (i = 0; i < 2; i++)
f += vr_quat(fs.eyes[i].orientation) * glm::vec3(0.0f, 0.0f, -1.0f);
f.y = 0.0f;
if (glm::length(f) < 0.001f) /* looking straight up/down */
return glm::vec3(0.0f, 0.0f, -1.0f);
return glm::normalize(f);
}
/* How long a lost headset is waited for before going flat. A video reinit
* restarts the headset session, and leaving VR changes the geometry, which
* reinits again. */
#define VR_HOLD_SECONDS 5
struct vr_hold
{
bool active;
unsigned missing; /* frames without HMD while active */
};
enum vr_hold_step
{
VR_HOLD_FLAT = 0,
VR_HOLD_ENTER,
VR_HOLD_TRACK,
VR_HOLD_WAIT, /* active, HMD missing: repeat the last frame */
VR_HOLD_LEAVE
};
static inline enum vr_hold_step vr_hold_update(struct vr_hold *h,
bool want, bool hmd, unsigned limit)
{
if (!h->active)
{
if (!want || !hmd)
return VR_HOLD_FLAT;
h->active = true;
h->missing = 0;
return VR_HOLD_ENTER;
}
if (!want || (!hmd && ++h->missing >= limit))
{
h->active = false;
return VR_HOLD_LEAVE;
}
if (hmd)
h->missing = 0;
return hmd ? VR_HOLD_TRACK : VR_HOLD_WAIT;
}
#endif