1 Commits

Author SHA1 Message Date
ml 920842a465 Show a laser-beam pointer while aiming at the menu/keyboard
build / build (push) Successful in 1m40s
The flat cross-shaped reticle used while aiming at the game quad is
drawn directly into the game quad's own texture, so it only ever made
sense there - while the menu launcher or keyboard was open, there was
no visual feedback at all until the ray was precisely on target,
making both panels hard to aim at.

Add a second, thin XrCompositionLayerQuad per hand: a billboarded
"laser beam" ribbon from the controller to wherever that hand's ray
currently crosses the plane of whichever overlay (keyboard, then menu)
claims it that frame, oriented via a view-space head pose so it reads
as a line from the viewer's eye regardless of angle, colored per hand
(cyan left, amber right) and clamped to 2m so grazing angles don't
produce an absurdly long beam. Built from data xr_input.c's existing
per-hand hit-testing already computes; the game-quad reticle itself is
untouched.
2026-08-14 07:30:13 +02:00
4 changed files with 332 additions and 31 deletions
+9 -4
View File
@@ -184,10 +184,15 @@ set only on the keyboard's layer - the menu launcher stays fully opaque)
so whatever's behind it - the game quad, mid-play - stays visible while
it's up.
The laser
pointer/cursor is currently only visible while
actually aiming at the game or menu quad respectively - there's no visual
feedback yet while aiming at empty space between them. (Only tested on
While the menu launcher or keyboard is open, each hand also gets a thin,
colored laser-beam quad (cyan left, amber right) from the controller to
wherever its ray currently crosses that panel's plane - a second,
billboarded `XrCompositionLayerQuad` per hand (`xr_input_build_beam()` in
`xr_input.c`), oriented so it reads as a line from the viewer's eye
regardless of angle, since the flat cross-shaped reticle used while
aiming at the game quad is drawn directly into the game quad's own
texture and can't represent a ray traversing real 3D space. Aiming at the
game quad itself still uses that flat reticle, unchanged. (Only tested on
Meta Quest so far -
the steps below use Quest-specific tool names where relevant, but the
same `adb install` flow applies to any Android headset with USB
+270 -7
View File
@@ -9,6 +9,7 @@
#include "xr_overlay.h"
#include "xr_session.h"
#include "xr_swapchain.h"
#define TAG "QuestShock"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
@@ -72,6 +73,29 @@ static float g_keyboard_drag_offset_y = 0.0f;
static GLuint g_reticle_program = 0;
static GLint g_reticle_color_loc = -1;
// A "view" reference space, located once per frame (not per hand) to get
// an approximate head position for the laser-beam billboard math below -
// the only consumer of a head pose in this file. Both LOCAL (g_local_space,
// owned by xr_session.c) and VIEW reference spaces are mandated by core
// OpenXR, so no capability check is needed to create this.
static XrSpace g_view_space = XR_NULL_HANDLE;
// Per-hand laser-beam state: a thin, billboarded quad spanning from the
// controller to wherever that hand's ray currently crosses the plane of
// whichever overlay (keyboard/menu) claimed it this frame - see
// xr_input_build_beam()/xr_input_get_beam_layer(). Each hand gets its own
// tiny solid-color swapchain (no shared tint on XrCompositionLayerQuad,
// so two separately-colored textures is simplest). g_beam_quad_valid is
// reset to false at the top of every xr_input_sync_and_draw() call and
// only set back to true if that hand actually claims a beam this frame.
#define BEAM_TEX_SIZE 2
#define BEAM_THICKNESS_METERS 0.004f
#define MAX_BEAM_LENGTH_METERS 2.0f
static XrSwapchainState g_beam_swapchain[2];
static XrCompositionLayerQuad g_beam_quad[2];
static bool g_beam_quad_valid[2] = {false, false};
static bool xr_check(XrResult result, const char *what) {
if (XR_SUCCEEDED(result))
return true;
@@ -95,6 +119,70 @@ static void quat_rotate_vec(const XrQuaternionf *q, float vx, float vy, float vz
*outz = vz + 2.0f * (q->x * cy - q->y * cx);
}
static void vec3_sub(const float a[3], const float b[3], float out[3]) {
out[0] = a[0] - b[0];
out[1] = a[1] - b[1];
out[2] = a[2] - b[2];
}
static float vec3_dot(const float a[3], const float b[3]) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
static void vec3_cross(const float a[3], const float b[3], float out[3]) {
out[0] = a[1] * b[2] - a[2] * b[1];
out[1] = a[2] * b[0] - a[0] * b[2];
out[2] = a[0] * b[1] - a[1] * b[0];
}
// Returns false (out left untouched) if v is too close to zero-length to
// normalize safely - callers use this to detect a degenerate billboard
// axis and fall back to another reference vector.
static bool vec3_normalize(const float v[3], float out[3]) {
float len = sqrtf(vec3_dot(v, v));
if (len < 1e-6f)
return false;
out[0] = v[0] / len;
out[1] = v[1] / len;
out[2] = v[2] / len;
return true;
}
// Converts an orthonormal local->world rotation matrix (given as its
// columns - x/y/z, each the world-space direction of that local axis) to
// an XrQuaternionf, via the standard trace-based (Shepperd) method.
static void mat3_to_quat(const float x[3], const float y[3], const float z[3], XrQuaternionf *q) {
float m00 = x[0], m10 = x[1], m20 = x[2];
float m01 = y[0], m11 = y[1], m21 = y[2];
float m02 = z[0], m12 = z[1], m22 = z[2];
float trace = m00 + m11 + m22;
if (trace > 0.0f) {
float s = sqrtf(trace + 1.0f) * 2.0f;
q->w = 0.25f * s;
q->x = (m21 - m12) / s;
q->y = (m02 - m20) / s;
q->z = (m10 - m01) / s;
} else if (m00 > m11 && m00 > m22) {
float s = sqrtf(1.0f + m00 - m11 - m22) * 2.0f;
q->w = (m21 - m12) / s;
q->x = 0.25f * s;
q->y = (m01 + m10) / s;
q->z = (m02 + m20) / s;
} else if (m11 > m22) {
float s = sqrtf(1.0f + m11 - m00 - m22) * 2.0f;
q->w = (m02 - m20) / s;
q->x = (m01 + m10) / s;
q->y = 0.25f * s;
q->z = (m12 + m21) / s;
} else {
float s = sqrtf(1.0f + m22 - m00 - m11) * 2.0f;
q->w = (m10 - m01) / s;
q->x = (m02 + m20) / s;
q->y = (m12 + m21) / s;
q->z = 0.25f * s;
}
}
static GLuint compile_shader(GLenum type, const char *src) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &src, NULL);
@@ -152,7 +240,7 @@ static bool xr_input_init_reticle_program(void) {
return true;
}
bool xr_input_init(XrInstance instance, XrSession session) {
bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_format) {
g_instance = instance;
g_session = session;
@@ -246,6 +334,21 @@ bool xr_input_init(XrInstance instance, XrSession session) {
if (!xr_check(xrAttachSessionActionSets(session, &attachInfo), "xrAttachSessionActionSets"))
return false;
XrReferenceSpaceCreateInfo viewSpaceInfo = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
viewSpaceInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW;
viewSpaceInfo.poseInReferenceSpace.orientation.w = 1.0f;
if (!xr_check(xrCreateReferenceSpace(session, &viewSpaceInfo, &g_view_space),
"xrCreateReferenceSpace(view)"))
return false;
for (int hand = 0; hand < 2; hand++) {
if (!xr_swapchain_create(instance, session, swapchain_format, BEAM_TEX_SIZE, BEAM_TEX_SIZE,
&g_beam_swapchain[hand])) {
LOGE("XR: beam swapchain setup failed for hand %d", hand);
return false;
}
}
if (!xr_input_init_reticle_program())
return false;
@@ -266,7 +369,9 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
const XrSpaceLocation *location, float fx, float fy, float fz,
bool selectDownEdge, bool selectUpEdge, bool *touchActive,
bool *prevHit, const char *quadName, float *outCursorU,
float *outCursorV, bool *outCursorHit) {
float *outCursorV, bool *outCursorHit, bool *outPlaneHit,
float *outPlaneDistance) {
*outPlaneHit = false;
if (!xr_overlay_is_visible(overlay))
return false;
@@ -283,9 +388,9 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
// view's pixel grid and the on-quad hit logs use) are only meaningful
// within the quad's current bounds, unlike planeHit.
bool planeHit = false, hit = false;
float u = 0.0f, v = 0.0f, worldX = 0.0f, worldY = 0.0f;
float u = 0.0f, v = 0.0f, worldX = 0.0f, worldY = 0.0f, t = 0.0f;
if (fabsf(fz) > 1e-5f) {
float t = (-distance - location->pose.position.z) / fz;
t = (-distance - location->pose.position.z) / fz;
if (t > 0.0f) {
planeHit = true;
worldX = location->pose.position.x + t * fx;
@@ -297,6 +402,8 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
v = (1.0f - cy) * 0.5f;
}
}
*outPlaneHit = planeHit;
*outPlaneDistance = t;
if (hit != *prevHit) {
LOGI("XR: %s aim ray %s %s quad (u=%.2f v=%.2f)", kHandName[hand],
@@ -353,6 +460,114 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
return true;
}
// Builds this hand's laser-beam quad - a thin, billboarded ribbon from the
// controller (location->pose.position) along the aim ray (fx,fy,fz,
// already unit length) to length_m meters out, oriented so it reads as a
// line from the viewer's eye (headLoc->pose.position) regardless of angle:
// one in-plane axis follows the ray direction, the other is the thin
// "width", and the quad's normal is billboarded toward the eye
// (orthogonalized against the ray axis, so the ribbon only rotates around
// its own long axis as the hand moves, never twists). Acquires/clears/
// releases this hand's tiny beam swapchain and leaves g_beam_quad[hand]
// ready for xr_input_get_beam_layer() - doesn't set space/eyeVisibility/
// layerFlags (see that function's doc comment in xr_input.h).
static void xr_input_build_beam(int hand, const XrSpaceLocation *location, float fx, float fy,
float fz, float length_m, const XrSpaceLocation *headLoc) {
float origin[3] = {location->pose.position.x, location->pose.position.y,
location->pose.position.z};
float dirY[3] = {fx, fy, fz};
float endpoint[3] = {origin[0] + length_m * fx, origin[1] + length_m * fy,
origin[2] + length_m * fz};
float center[3] = {(origin[0] + endpoint[0]) * 0.5f, (origin[1] + endpoint[1]) * 0.5f,
(origin[2] + endpoint[2]) * 0.5f};
float head[3] = {headLoc->pose.position.x, headLoc->pose.position.y,
headLoc->pose.position.z};
float toEyeRaw[3], toEye[3];
vec3_sub(head, center, toEyeRaw);
if (!vec3_normalize(toEyeRaw, toEye)) {
// Head is (almost) exactly at the beam's midpoint - astronomically
// unlikely, but fall back to a fixed direction rather than divide
// by ~0.
toEye[0] = 0.0f;
toEye[1] = 0.0f;
toEye[2] = 1.0f;
}
// Orthogonalize toEye against the ray axis to get the billboard
// normal - if the ray points almost straight at/away from the eye,
// that leaves ~nothing to normalize, so fall back to world-up then
// world-Z, each reprojected the same way.
float normalZ[3];
float d = vec3_dot(toEye, dirY);
float proj[3] = {toEye[0] - dirY[0] * d, toEye[1] - dirY[1] * d, toEye[2] - dirY[2] * d};
if (!vec3_normalize(proj, normalZ)) {
static const float kWorldUp[3] = {0.0f, 1.0f, 0.0f};
d = vec3_dot(kWorldUp, dirY);
proj[0] = kWorldUp[0] - dirY[0] * d;
proj[1] = kWorldUp[1] - dirY[1] * d;
proj[2] = kWorldUp[2] - dirY[2] * d;
if (!vec3_normalize(proj, normalZ)) {
static const float kWorldZ[3] = {0.0f, 0.0f, 1.0f};
d = vec3_dot(kWorldZ, dirY);
proj[0] = kWorldZ[0] - dirY[0] * d;
proj[1] = kWorldZ[1] - dirY[1] * d;
proj[2] = kWorldZ[2] - dirY[2] * d;
if (!vec3_normalize(proj, normalZ)) {
// dirY parallel to both world-up and world-Z is impossible
// for two non-parallel vectors - unreachable in practice,
// but keep the beam well-defined regardless.
normalZ[0] = 1.0f;
normalZ[1] = 0.0f;
normalZ[2] = 0.0f;
}
}
}
float axisX[3];
vec3_cross(dirY, normalZ, axisX);
if (!vec3_normalize(axisX, axisX)) {
axisX[0] = 1.0f;
axisX[1] = 0.0f;
axisX[2] = 0.0f;
}
float axisZ[3];
vec3_cross(axisX, dirY, axisZ); // already unit length - axisX/dirY are orthonormal
XrQuaternionf orientation;
mat3_to_quat(axisX, dirY, axisZ, &orientation);
if (xr_swapchain_acquire(g_instance, &g_beam_swapchain[hand])) {
// Premultiplied alpha (matches OverlayPanel.compositeAndPublish()'s
// convention, and XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT's
// assumption - see xr_session.c) - kHandColor's RGB scaled by this
// beam's own alpha, not the opaque RGB itself.
const float alpha = 0.55f;
const float *c = kHandColor[hand];
glClearColor(c[0] * alpha, c[1] * alpha, c[2] * alpha, alpha);
glClear(GL_COLOR_BUFFER_BIT);
xr_swapchain_release(g_instance, &g_beam_swapchain[hand]);
}
XrCompositionLayerQuad *quad = &g_beam_quad[hand];
quad->type = XR_TYPE_COMPOSITION_LAYER_QUAD;
quad->next = NULL;
quad->subImage.swapchain = g_beam_swapchain[hand].swapchain;
quad->subImage.imageRect.offset.x = 0;
quad->subImage.imageRect.offset.y = 0;
quad->subImage.imageRect.extent.width = BEAM_TEX_SIZE;
quad->subImage.imageRect.extent.height = BEAM_TEX_SIZE;
quad->subImage.imageArrayIndex = 0;
quad->pose.position.x = center[0];
quad->pose.position.y = center[1];
quad->pose.position.z = center[2];
quad->pose.orientation = orientation;
quad->size.width = BEAM_THICKNESS_METERS;
quad->size.height = length_m;
g_beam_quad_valid[hand] = true;
}
void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
if (g_action_set == XR_NULL_HANDLE)
return;
@@ -375,6 +590,25 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
g_keyboard_drag_hand = -1;
}
// A hand only ever gets a laser-beam quad this frame if it actually
// claims the keyboard or menu overlay's ray below (see the
// xr_input_build_beam() calls) - reset here so a hand that doesn't
// claim one this frame doesn't keep showing last frame's beam.
g_beam_quad_valid[LEFT] = g_beam_quad_valid[RIGHT] = false;
// The billboard math needs an approximate head position - only bother
// locating it on frames where a beam could possibly be drawn at all.
bool needBeams = draw && (menuVisible || keyboardVisible);
XrSpaceLocation headLoc = {XR_TYPE_SPACE_LOCATION};
bool haveHead = false;
if (needBeams) {
const XrSpaceLocationFlags neededHead =
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
haveHead = xr_check(xrLocateSpace(g_view_space, baseSpace, time, &headLoc),
"xrLocateSpace(view)") &&
(headLoc.locationFlags & neededHead) == neededHead;
}
// The GL 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
@@ -454,16 +688,33 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
// Keyboard first (it's the more likely target while it's up), then
// the menu launcher, then - only if neither is visible - the game
// quad's own reticle below. A single ray only ever interacts with
// one target per hand per frame.
// one target per hand per frame. Whichever overlay claims the ray
// also gets a laser-beam quad built for it (if a head pose is
// available and the ray actually crosses that overlay's plane) -
// see xr_input_build_beam().
bool keyboardPlaneHit = false;
float keyboardPlaneDistance = 0.0f;
if (xr_input_try_overlay(keyboardOverlay, true, hand, &location, fx, fy, fz,
selectDownEdge, selectUpEdge, &g_keyboard_touch_active[hand],
&g_keyboard_prev_hit[hand], "keyboard", &keyboardCursorU,
&keyboardCursorV, &keyboardCursorHit))
&keyboardCursorV, &keyboardCursorHit, &keyboardPlaneHit,
&keyboardPlaneDistance)) {
if (haveHead && keyboardPlaneHit)
xr_input_build_beam(hand, &location, fx, fy, fz,
fminf(keyboardPlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
continue;
}
bool menuPlaneHit = false;
float menuPlaneDistance = 0.0f;
if (xr_input_try_overlay(menuOverlay, false, hand, &location, fx, fy, fz, selectDownEdge,
selectUpEdge, &g_menu_touch_active[hand], &g_menu_prev_hit[hand],
"menu", &menuCursorU, &menuCursorV, &menuCursorHit))
"menu", &menuCursorU, &menuCursorV, &menuCursorHit, &menuPlaneHit,
&menuPlaneDistance)) {
if (haveHead && menuPlaneHit)
xr_input_build_beam(hand, &location, fx, fy, fz,
fminf(menuPlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
continue;
}
float distance, halfWidth, halfHeight;
xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight);
@@ -508,12 +759,23 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
keyboardCursorHit);
}
bool xr_input_get_beam_layer(int hand, XrCompositionLayerQuad *out_quad) {
if (hand < 0 || hand > 1 || !g_beam_quad_valid[hand])
return false;
*out_quad = g_beam_quad[hand];
return true;
}
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;
xr_swapchain_destroy(&g_beam_swapchain[hand]);
}
if (g_view_space != XR_NULL_HANDLE)
xrDestroySpace(g_view_space);
g_view_space = XR_NULL_HANDLE;
if (g_action_set != XR_NULL_HANDLE)
xrDestroyActionSet(g_action_set);
g_action_set = XR_NULL_HANDLE;
@@ -533,6 +795,7 @@ void xr_input_shutdown(void) {
memset(g_menu_touch_active, 0, sizeof(g_menu_touch_active));
memset(g_keyboard_prev_hit, 0, sizeof(g_keyboard_prev_hit));
memset(g_keyboard_touch_active, 0, sizeof(g_keyboard_touch_active));
g_beam_quad_valid[LEFT] = g_beam_quad_valid[RIGHT] = false;
g_prev_menu = false;
g_keyboard_dragging = false;
g_keyboard_drag_hand = -1;
+23 -7
View File
@@ -5,13 +5,17 @@
// visible, this tests against the game quad xr_session.c submits and draws
// a small reticle where each hand's aim ray crosses it; while either
// overlay is visible, it tests against that overlay's quad instead (the
// keyboard is tried first) and forwards trigger edges as synthetic touches
// (no reticle - each overlay's own content comes from its Java view's
// rendered Bitmap).
// keyboard is tried first), forwards trigger edges as synthetic touches,
// and - since neither overlay's own on-quad cursor gives any feedback
// while the ray is short of actually landing on it - builds a thin,
// billboarded laser-beam quad per hand from the controller to wherever
// the ray currently crosses that overlay's plane (see
// xr_input_get_beam_layer()).
#ifndef QUESTSHOCK_XR_INPUT_H
#define QUESTSHOCK_XR_INPUT_H
#include <stdbool.h>
#include <stdint.h>
#define XR_USE_PLATFORM_ANDROID 1
#define XR_USE_GRAPHICS_API_OPENGL_ES 1
@@ -24,10 +28,12 @@ extern "C" {
// Call once, right after the session is created (see xr_session.c's
// xr_create_instance_and_session()) - creates the action set/actions,
// suggests Touch controller bindings, creates the per-hand aim action
// spaces, and attaches the set to the session. Returns false (logged,
// non-fatal - the caller keeps rendering without input) if any of that
// fails.
bool xr_input_init(XrInstance instance, XrSession session);
// spaces and a view-space reference (for the laser-beam billboard math),
// creates the two per-hand beam swapchains (swapchain_format - the same
// format shared by the game swapchain and both overlays), and attaches
// the action set to the session. Returns false (logged, non-fatal - the
// caller keeps rendering without input) if any of that fails.
bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_format);
// Call once per frame from xr_frame_end(), before releasing the acquired
// swapchain image - syncs this frame's action states (always, so edge
@@ -40,6 +46,16 @@ bool xr_input_init(XrInstance instance, XrSession session);
// xr_frame_begin() used to predict this frame.
void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw);
// Call once per hand (hand: 0=left, 1=right) after xr_input_sync_and_draw()
// in the same frame - if that hand's ray claimed the keyboard or menu
// overlay this frame (see the file comment above), fills *out_quad's
// subImage/pose/size for its laser-beam ribbon and returns true.
// space/eyeVisibility/layerFlags are left for the caller to set, same
// convention as xr_overlay_render_and_build_layer(). Returns false
// (out_quad untouched) if no beam should be shown for that hand this
// frame.
bool xr_input_get_beam_layer(int hand, XrCompositionLayerQuad *out_quad);
void xr_input_shutdown(void);
#ifdef __cplusplus
+30 -13
View File
@@ -213,19 +213,16 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
"xrCreateReferenceSpace"))
return false;
// Non-fatal if it fails (e.g. no controllers bound yet) - rendering
// keeps working either way, just without the laser pointer.
xr_input_init(g_instance, g_session);
// Prefer a plain linear 8-bit format over GL_SRGB8_ALPHA8: the source
// SDL surface pixels are already sRGB-encoded (as ordinary 8-bit image
// data conventionally is) and get uploaded/sampled as plain linear
// GL_RGBA with no decode step anywhere in this path, matching the
// desktop SDL_RenderCopy path this replaces - an sRGB swapchain format
// would auto-gamma-encode on write and double-encode already-encoded
// data. Every swapchain below (game quad, menu/keyboard overlays)
// shares this one choice - the compositor's supported format set
// doesn't depend on swapchain size.
// data. Every swapchain below (game quad, menu/keyboard overlays, and
// - via xr_input_init() - the per-hand laser-beam overlays) shares
// this one choice - the compositor's supported format set doesn't
// depend on swapchain size.
uint32_t formatCount = 0;
xrEnumerateSwapchainFormats(g_session, 0, &formatCount, NULL);
int64_t *formats = (int64_t *)malloc(sizeof(int64_t) * formatCount);
@@ -240,6 +237,10 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
}
free(formats);
// Non-fatal if it fails (e.g. no controllers bound yet) - rendering
// keeps working either way, just without the laser pointer/beams.
xr_input_init(g_instance, g_session, chosenFormat);
if (!xr_swapchain_create(g_instance, g_session, chosenFormat, game_width, game_height,
&g_game_swapchain)) {
LOGE("XR: game swapchain setup failed");
@@ -367,12 +368,14 @@ void xr_frame_end(void) {
if (!xr_is_session_running())
return;
// Up to 3 layers: the game quad (if a frame was actually rendered),
// and, while visible, the menu launcher and keyboard overlays - each
// gets its own acquire/render/release cycle against its own swapchain
// (see xr_overlay_render_and_build_layer()) any time before xrEndFrame,
// Up to 5 layers: the game quad (if a frame was actually rendered),
// the menu launcher and keyboard overlays while visible - each gets
// its own acquire/render/release cycle against its own swapchain (see
// xr_overlay_render_and_build_layer()) any time before xrEndFrame,
// unlike the game quad there's no per-frame engine rendering to wrap
// around here, just each overlay's own blit.
// around here, just each overlay's own blit - and, per hand, a
// laser-beam quad while that hand's ray is aimed at one of those two
// overlays (see xr_input_get_beam_layer()).
XrCompositionLayerQuad gameQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
gameQuad.space = g_local_space;
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
@@ -385,7 +388,7 @@ void xr_frame_end(void) {
gameQuad.size.height =
QUAD_WIDTH_METERS * (float)g_game_swapchain.height / (float)g_game_swapchain.width;
const XrCompositionLayerBaseHeader *layers[3];
const XrCompositionLayerBaseHeader *layers[5];
uint32_t layerCount = 0;
if (g_have_acquired_game_image)
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&gameQuad;
@@ -413,6 +416,20 @@ void xr_frame_end(void) {
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&keyboardQuad;
}
// Submitted last (frontmost) so a beam doesn't z-fight against the
// panel surface it's aimed at/terminates on - see
// xr_input_get_beam_layer(). Translucent for the same reason the
// keyboard is (see comment above).
XrCompositionLayerQuad beamQuad[2];
for (int hand = 0; hand < 2; hand++) {
if (xr_input_get_beam_layer(hand, &beamQuad[hand])) {
beamQuad[hand].space = g_local_space;
beamQuad[hand].eyeVisibility = XR_EYE_VISIBILITY_BOTH;
beamQuad[hand].layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&beamQuad[hand];
}
}
XrFrameEndInfo endInfo = {XR_TYPE_FRAME_END_INFO};
endInfo.displayTime = g_predicted_display_time;
endInfo.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;