openxr: the track space is handed to the frame loop, the GL host drops its lock

A swap publishes the new track space as a record - space, origin,
number and carry range - with one pointer exchange and parks the old
one on a lock-free retired list; the frame loop's thread takes the
current record at each frame and destroys the retired spaces then, as
their only other user. A projection drawn before a recenter's remake
shows in the new space by the record's carry range, so nothing
rewrites it and the projection keeps one writer.

With that, every runtime call of the desktop GL host is on its XR
thread, so the host has no lock. Vulkan keeps its queue lock, which
the core's lock_queue shares.

openxr_core_test swaps from a core thread while the frame loop
submits: every frame shows the carried projection in a live space,
and each space is destroyed once.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-10-29 21:02:20 +01:00
1 parent f23cc50ca4
commit 3863c2c1fa
7 files changed
+376 -162

No files matched your search

+7 -39
View File
@@ -19,7 +19,6 @@
#include <boolean.h>
#include <retro_atomic.h>
#include <retro_miscellaneous.h>
#include <rthreads/rthreads.h>
#include <string/stdstring.h>
#ifdef HAVE_CONFIG_H
@@ -126,8 +125,6 @@ struct gl_openxr
openxr_host_t host;
/* The driver shows the headset nothing (HDR output). */
retro_atomic_int_t hidden;
/* The graphics lock: the session's calls and the frame's span. */
slock_t *lock;
const gfx_ctx_driver_t *ctx_driver;
void *ctx_data;
gfx_ctx_shared_context_t shared;
@@ -146,16 +143,6 @@ struct gl_openxr
gl_openxr_slot_t slots[OPENXR_MAX_SLOTS];
};
static void gl_openxr_lock(void *user)
{
slock_lock(((gl_openxr_t*)user)->lock);
}
static void gl_openxr_unlock(void *user)
{
slock_unlock(((gl_openxr_t*)user)->lock);
}
static void gl_openxr_state_hook(void *user, XrSessionState state)
{
if (state == XR_SESSION_STATE_STOPPING)
@@ -351,14 +338,12 @@ gl_openxr_t *gl_openxr_new(const gfx_ctx_driver_t *ctx_driver,
xr->session.hooks.exiting = gl_openxr_exiting_hook;
xr->session.hooks.local_changed = gl_openxr_local_hook;
xr->session.hooks.stage_changed = gl_openxr_stage_hook;
xr->session.hooks.lock = gl_openxr_lock;
xr->session.hooks.unlock = gl_openxr_unlock;
/* No lock: every runtime call is on the XR thread, which alone
* makes current the context the runtime submits with. */
xr->session.hooks.user = xr;
for (s = 0; s < OPENXR_MAX_SLOTS; s++)
openxr_ring_init(&xr->slots[s].ring);
openxr_host_init(&xr->host, &xr->rt, &xr->session, &xr->frame);
if (!(xr->lock = slock_new()))
goto end;
if (!ctx_driver->shared_context_new(ctx_data, &xr->shared))
{
@@ -521,9 +506,7 @@ static void gl_openxr_thread_stop(void *user)
}
if (xr->frame.cursor)
{
openxr_session_lock(&xr->session);
xr->sc.DestroySwapchain(xr->frame.cursor);
openxr_session_unlock(&xr->session);
xr->frame.cursor = XR_NULL_HANDLE;
}
if (xr->session.session)
@@ -542,15 +525,13 @@ static void gl_openxr_hide(gl_openxr_t *xr, unsigned slot)
openxr_slot_forget(&xr->sc, slot);
if (slot == OPENXR_PROJECTION_SLOT)
{
openxr_session_lock(&xr->session);
openxr_frame_clear_projection(&xr->frame);
openxr_session_unlock(&xr->session);
xr->proj_shown = false;
}
}
/* The slot's swapchains at a record's size, on the XR thread; they take
* the graphics lock themselves. A size refused is not asked again. */
/* The slot's swapchains at a record's size, on the XR thread. A size
* refused is not asked again. */
static bool gl_openxr_make_slot(gl_openxr_t *xr, unsigned slot,
unsigned dims, unsigned layers)
{
@@ -680,31 +661,22 @@ static bool gl_openxr_fill_slot(gl_openxr_t *xr, unsigned slot)
/* The draw finishes before the copy reads it. */
xr->gl.WaitSync(r->drawn, 0, GL_TIMEOUT_IGNORED);
openxr_session_lock(&xr->session);
if (!openxr_slot_acquire(&xr->sc, slot, index))
{
openxr_session_unlock(&xr->session);
return false;
}
ok = gl_openxr_copy(xr, s, r, index);
r->copied = xr->gl.FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
xr->gl.Flush();
openxr_slot_release(&xr->sc, slot);
/* The eyes go out with the poses that drew them, in the track space
* the recenters since have carried them to. */
/* The eyes go out with the poses that drew them; the frame carries
* them to a recenter's new track space. */
if (slot == OPENXR_PROJECTION_SLOT)
{
if (ok && r->projection.active)
{
openxr_projection_t p = r->projection;
p.track_seq = openxr_frame_carried_seq(&xr->frame, p.track_seq);
openxr_frame_publish_projection(&xr->frame, &p);
}
openxr_frame_publish_projection(&xr->frame, &r->projection);
else
openxr_frame_clear_projection(&xr->frame);
xr->proj_shown = ok && r->projection.active;
}
openxr_session_unlock(&xr->session);
s->pending = false;
if (!ok)
@@ -814,9 +786,7 @@ void gl_openxr_stop_thread(gl_openxr_t *xr)
openxr_frame_stop(&xr->frame);
video_driver_set_xr_host(NULL);
openxr_frame_clear_quads(&xr->frame);
slock_lock(xr->lock);
openxr_frame_clear_projection(&xr->frame);
slock_unlock(xr->lock);
xr->started = false;
}
@@ -841,8 +811,6 @@ void gl_openxr_free(gl_openxr_t *xr)
xr->ctx_driver->shared_context_free(&xr->shared);
openxr_runtime_deinit(&xr->rt);
openxr_frame_deinit(&xr->frame);
if (xr->lock)
slock_free(xr->lock);
free(xr);
}
+30 -40
View File
@@ -169,8 +169,9 @@ static const XrSwapchain *openxr_frame_chains(openxr_slot_t *slot,
return (*layers && slot->old[0]) ? slot->old : NULL;
}
/* The published quads whose slot shows an image. The caller
* holds the graphics lock, so no slot changes under it. */
/* The published quads whose slot shows an image. No slot changes
* under it: slots change under the graphics lock, or on this thread
* where the host has none. */
static unsigned openxr_frame_layers(openxr_frame_t *f,
XrCompositionLayerQuad *layers,
const XrCompositionLayerBaseHeader **ptrs)
@@ -223,9 +224,17 @@ static unsigned openxr_frame_layers(openxr_frame_t *f,
return n;
}
/* The published projection, if its slot has content and its space is
* the session's. The caller holds the graphics lock, so neither
* changes under it. */
/* Whether what was drawn in the track space numbered seq shows in t:
* drawn in it, or in one a recenter's remake carried to it. */
static bool openxr_frame_track_shows(const openxr_track_t *t, int seq)
{
return t && ( seq == t->seq
|| ( t->carry_to == t->seq
&& seq >= t->carry_from && seq < t->carry_to));
}
/* The published projection, if its slot has content and it was drawn
* in the frame's track space or one carried to it. */
static bool openxr_frame_projection(openxr_frame_t *f,
XrCompositionLayerProjection *layer,
XrCompositionLayerProjectionView *views)
@@ -236,13 +245,12 @@ static bool openxr_frame_projection(openxr_frame_t *f,
const XrSwapchain *chains = NULL;
openxr_projection_t p;
openxr_slot_t *slot = &f->sc->slots[OPENXR_PROJECTION_SLOT];
const openxr_track_t *t = f->track;
openxr_seq_read(&f->proj_seq, f->proj_words, &p, sizeof(p));
if ( !p.active
|| !(chains = openxr_frame_chains(slot, &dims, &count))
|| count < 2
|| !f->session->track_space
|| p.track_seq != retro_atomic_load_acquire_int(
&f->session->track_seq))
|| !openxr_frame_track_shows(t, p.track_seq))
return false;
w = MIN(p.width, VIDEO_SCALE_W(dims));
h = MIN(p.height, VIDEO_SCALE_H(dims));
@@ -265,7 +273,7 @@ static bool openxr_frame_projection(openxr_frame_t *f,
layer->type = XR_TYPE_COMPOSITION_LAYER_PROJECTION;
layer->next = NULL;
layer->layerFlags = 0;
layer->space = f->session->track_space;
layer->space = t->space;
layer->viewCount = 2;
layer->views = views;
return true;
@@ -317,31 +325,31 @@ static void openxr_frame_recenter(openxr_frame_t *f, XrTime time)
/* Where Recenter puts the screens: a seated world's origin while
* tracked content holds one, so a recenter moves both in one frame;
* else the anchor. Under the graphics lock, as the space is replaced. */
* else the anchor, by the frame's track space. */
static void openxr_frame_screens_anchor(openxr_frame_t *f,
video_xr_pose_t *out)
{
const openxr_session_t *s = f->session;
const openxr_track_t *t = f->track;
/* Lives until the session goes; core_vr_content's sample remakes it. */
if (!s->track_space || !s->track_local)
if (!t || !t->local)
{
*out = f->tracked.anchor;
return;
}
out->orientation.x = s->track_origin.orientation.x;
out->orientation.y = s->track_origin.orientation.y;
out->orientation.z = s->track_origin.orientation.z;
out->orientation.w = s->track_origin.orientation.w;
out->position.x = s->track_origin.position.x;
out->position.y = s->track_origin.position.y;
out->position.z = s->track_origin.position.z;
out->orientation.x = t->origin.orientation.x;
out->orientation.y = t->origin.orientation.y;
out->orientation.z = t->origin.orientation.z;
out->orientation.w = t->origin.orientation.w;
out->position.x = t->origin.position.x;
out->position.y = t->origin.position.y;
out->position.z = t->origin.position.z;
}
/* Where the screens are for the frame shown at time, published for the
* laser: Fixed in Room where Recenter puts them; Follow Head in VIEW
* space; Lazy Follow at an anchor that stays until the head turns well
* away, then glides back in front of it, and that each recenter puts
* where Recenter puts them. Under the graphics lock. */
* where Recenter puts them. */
static void openxr_frame_place_screens(openxr_frame_t *f, XrTime time)
{
video_xr_pose_t at;
@@ -517,6 +525,7 @@ static bool openxr_frame_wait_begin(openxr_frame_t *f,
}
b->waited = true;
b->display_time = state.predictedDisplayTime;
f->track = openxr_session_take_track(f->session);
b->period = state.predictedDisplayPeriod;
session_state = retro_atomic_load_acquire_int(&f->session->state);
/* A headset that is not worn reports periods that are not its own. */
@@ -753,8 +762,7 @@ void openxr_frame_session_created(openxr_frame_t *f)
video_xr_pose_identity(&f->screens.pose);
f->screens.view = 0;
f->lazy_valid = false;
f->carry_from = 0;
f->carry_to = 0;
f->track = NULL;
openxr_frame_publish_tracked(f);
openxr_seq_publish(&f->screens_seq, f->screens_words,
&f->screens, sizeof(f->screens));
@@ -1111,24 +1119,6 @@ void openxr_frame_clear_projection(openxr_frame_t *f)
openxr_seq_publish(&f->proj_seq, f->proj_words, &none, sizeof(none));
}
void openxr_frame_move_projection(openxr_frame_t *f, int from, int to)
{
openxr_projection_t p;
if (from != f->carry_to)
f->carry_from = from;
f->carry_to = to;
openxr_seq_read(&f->proj_seq, f->proj_words, &p, sizeof(p));
if (p.track_seq != from)
return;
p.track_seq = to;
openxr_seq_publish(&f->proj_seq, f->proj_words, &p, sizeof(p));
}
int openxr_frame_carried_seq(openxr_frame_t *f, int seq)
{
return (seq >= f->carry_from && seq < f->carry_to) ? f->carry_to : seq;
}
void openxr_frame_get_anchor(openxr_frame_t *f, video_xr_pose_t *anchor)
{
openxr_tracked_t t;
+6 -11
View File
@@ -128,9 +128,8 @@ typedef struct openxr_frame
/* The drawing thread's projection, the same way. */
retro_atomic_int_t proj_seq;
retro_atomic_int_t proj_words[2 * OPENXR_WORDS(openxr_projection_t)];
/* Consecutive carried spaces, under the graphics lock. */
int carry_from;
int carry_to;
/* The track space this frame uses (openxr_session_take_track). */
const openxr_track_t *track; /* the frame loop's thread */
retro_atomic_int_t recenter;
/* Each move of the anchor, the hotkey's or the runtime's. */
retro_atomic_int_t recenters;
@@ -254,17 +253,13 @@ void openxr_frame_request_rate(openxr_frame_t *f, float hz);
void openxr_frame_publish(openxr_frame_t *f, const video_xr_quad_set_t *set);
/* The projection the XR thread submits from now on, behind the
* screens; left out until its slot releases an image, and once the
* track space it was drawn in is replaced. Published in the same
* graphics-lock span as its slot's release, so a new image never goes
* out with the previous poses. */
* track space it was drawn in is replaced without a carry. Published
* right after its slot's release, in the same graphics-lock span where
* the host has a lock, so a new image never goes out with the previous
* poses. */
void openxr_frame_publish_projection(openxr_frame_t *f,
const openxr_projection_t *p);
void openxr_frame_clear_projection(openxr_frame_t *f);
/* The drawing thread: a projection drawn in the track space numbered
* from goes on, with the same poses, in the one numbered to. */
void openxr_frame_move_projection(openxr_frame_t *f, int from, int to);
/* A queued projection's space after carries; caller holds the graphics lock. */
int openxr_frame_carried_seq(openxr_frame_t *f, int seq);
void openxr_frame_get_anchor(openxr_frame_t *f, video_xr_pose_t *anchor);
void openxr_frame_get_quads(openxr_frame_t *f, video_xr_quad_set_t *out);
/* Any thread: where the screens go (enum video_openxr_placement). */
+3 -3
View File
@@ -51,8 +51,9 @@ void openxr_host_stage_changed(openxr_host_t *h)
}
/* A recenter's remake: what the core drew in the old seated space keeps
* its poses in the new, so its world moves with the screens. Run in the
* swap's locked span: no headset frame shows the new space without it. */
* its poses in the new, so its world moves with the screens. The views
* handed out move here; a projection already published carries with
* the space the swap publishes. */
static void openxr_host_carry(void *user, int from, int to)
{
openxr_host_t *h = (openxr_host_t*)user;
@@ -62,7 +63,6 @@ static void openxr_host_carry(void *user, int from, int to)
for (i = 0; i < 4; i++)
if (h->handed[i].seq == from)
h->handed[i].seq = to;
openxr_frame_move_projection(h->frame, from, to);
}
static bool openxr_host_make_space(openxr_host_t *h,
+47 -12
View File
@@ -13,6 +13,7 @@
* If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdlib.h>
#include <string.h>
#include <retro_atomic.h>
@@ -232,8 +233,14 @@ error:
void openxr_session_destroy(openxr_session_t *s)
{
if (s->track_space)
s->DestroySpace(s->track_space);
openxr_track_t *t;
openxr_session_take_track(s);
if ((t = (openxr_track_t*)retro_atomic_exchange_ptr(&s->track, NULL)))
{
s->DestroySpace(t->space);
free(t);
}
s->carry_from = s->carry_to = 0;
if (s->view_space)
s->DestroySpace(s->view_space);
if (s->local_space)
@@ -252,6 +259,7 @@ bool openxr_session_swap_track_space(openxr_session_t *s,
{
XrResult res;
XrReferenceSpaceCreateInfo rci;
openxr_track_t *t, *old;
XrSpace fresh = XR_NULL_HANDLE;
enum retro_vr_reference_space got = RETRO_VR_REFERENCE_SPACE_LOCAL;
bool stage_failed = false;
@@ -292,22 +300,49 @@ bool openxr_session_swap_track_space(openxr_session_t *s,
if (stage_failed)
RARCH_WARN("[OpenXR] No STAGE space: tracked content is seated.\n");
}
/* The XR thread submits in it, and places the screens by it, under
* the lock. */
openxr_session_lock(s);
if (s->track_space)
s->DestroySpace(s->track_space);
s->track_space = fresh;
s->track_origin = *origin;
s->track_local = (got == RETRO_VR_REFERENCE_SPACE_LOCAL);
retro_atomic_fetch_add_int(&s->track_seq, 1);
if (!(t = (openxr_track_t*)malloc(sizeof(*t))))
{
s->DestroySpace(fresh);
return false;
}
/* Consecutive carries make one range. */
if (swapped)
{
if (from != s->carry_to)
s->carry_from = from;
s->carry_to = from + 1;
}
t->space = fresh;
t->origin = *origin;
t->seq = from + 1;
t->carry_from = s->carry_from;
t->carry_to = s->carry_to;
t->local = (got == RETRO_VR_REFERENCE_SPACE_LOCAL);
s->track_space = fresh;
retro_atomic_store_release_int(&s->track_seq, from + 1);
/* The frame loop may be submitting in the old one: it goes at that
* thread's next frame, which takes this one. */
if ((old = (openxr_track_t*)retro_atomic_exchange_ptr(&s->track, t)))
mpsc_stack_push(&s->track_retired, &old->park);
if (swapped)
swapped(user, from, from + 1);
openxr_session_unlock(s);
*space = got;
return true;
}
const openxr_track_t *openxr_session_take_track(openxr_session_t *s)
{
mpsc_stack_node_t *n = mpsc_stack_drain(&s->track_retired);
while (n)
{
openxr_track_t *t = (openxr_track_t*)n;
n = n->next;
s->DestroySpace(t->space);
free(t);
}
return (const openxr_track_t*)retro_atomic_load_acquire_ptr(&s->track);
}
/* Keeps lost: a lost instance cannot make a new session. */
void openxr_session_reset(openxr_session_t *s)
{
+34 -9
View File
@@ -18,6 +18,7 @@
#include <boolean.h>
#include <retro_atomic.h>
#include <queues/mpsc_stack.h>
#include <retro_common_api.h>
#include <libretro.h>
@@ -44,13 +45,29 @@ typedef struct openxr_session_hooks
void (*stage_changed)(void *user);
/* The graphics queue's lock, around every runtime call that may use
* the queue (xrBeginSession and xrEndSession, a slot's create and
* destroy, xrBeginFrame to xrEndFrame) and replacing the track space,
* on whichever thread makes it. */
* destroy, xrBeginFrame to xrEndFrame). NULL where those all run on
* the frame loop's thread and nothing else shares the queue. */
void (*lock)(void *user);
void (*unlock)(void *user);
void *user;
} openxr_session_hooks_t;
/* The track space as the frame loop's thread uses it: made by a swap,
* published whole and never changed, retired by the next swap and
* freed, its space with it, at that thread's next frame. A projection
* numbered in [carry_from, carry_to) shows in it when carry_to is seq:
* a recenter's remake carries what was drawn in the space before. */
typedef struct openxr_track
{
mpsc_stack_node_t park; /* on the session's retired list */
XrSpace space;
XrPosef origin; /* a LOCAL space's pose in LOCAL */
int seq;
int carry_from;
int carry_to;
bool local;
} openxr_track_t;
typedef struct openxr_session
{
openxr_session_hooks_t hooks;
@@ -58,11 +75,14 @@ typedef struct openxr_session
XrSession session;
XrSpace local_space;
XrSpace view_space;
XrSpace track_space; /* tracked content's, once it asks */
/* The swapping thread's: tracked content's space, once it asks, and
* the carries it made. */
XrSpace track_space;
retro_atomic_ptr_t track; /* openxr_track_t *, the current one */
mpsc_stack_t track_retired;
retro_atomic_int_t track_seq; /* bumped each time it is replaced */
/* A LOCAL track_space's pose in LOCAL: a seated world's origin. */
XrPosef track_origin;
bool track_local;
int carry_from;
int carry_to;
retro_atomic_int_t state; /* XrSessionState */
retro_atomic_int_t alive; /* the host's frames reach the headset */
retro_atomic_int_t running; /* between xrBeginSession and xrEndSession */
@@ -89,12 +109,17 @@ void openxr_session_destroy(openxr_session_t *s);
/* Tracked content's space: STAGE at the room's origin when asked for
* and the runtime has one, else LOCAL with origin as its pose there;
* what was granted is written back. It replaces the last one under the
* lock, and swapped, when given, runs in that locked span with the old
* and new track_seq: a frame sees both or neither. */
* what was granted is written back. Published for the frame loop's
* next frame, which destroys the one it replaces; takes no lock.
* swapped, when given, runs on this thread with the old and new
* track_seq, and the projections drawn in the old carry to the new. */
bool openxr_session_swap_track_space(openxr_session_t *s,
enum retro_vr_reference_space *space, const XrPosef *origin,
void (*swapped)(void *user, int from, int to), void *user);
/* The frame loop's thread, at each frame: destroys the retired spaces
* and returns the current one, NULL before the first; it stays valid
* until this thread's next call. */
const openxr_track_t *openxr_session_take_track(openxr_session_t *s);
void openxr_session_lock(openxr_session_t *s);
void openxr_session_unlock(openxr_session_t *s);
/* After a session the runtime ended, and after the frame loop has stopped:
+249 -48
View File
@@ -274,9 +274,19 @@ static XrSpace t_stage_space;
static bool t_in_stage_set;
static XrPosef t_in_stage;
static unsigned t_in_stage_invalid;
/* Run as a track space is made, before the swap publishes it: a
* headset frame on another thread while the swap is under way. */
static void (*t_at_remake)(void);
static XRAPI_ATTR XrResult XRAPI_CALL t_create_space(XrSession s,
const XrReferenceSpaceCreateInfo *ci, XrSpace *out)
{
if (t_at_remake)
{
void (*at)(void) = t_at_remake;
t_at_remake = NULL;
at();
t_at_remake = at;
}
if (t_space_fail_at && t_spaces_made + 1 == t_space_fail_at)
return XR_ERROR_RUNTIME_FAILURE;
if (ci->referenceSpaceType == XR_REFERENCE_SPACE_TYPE_STAGE)
@@ -1678,7 +1688,7 @@ static void test_track_space(void)
origin.position.z = -0.5f;
t_stage_missing = false;
/* LOCAL at the origin given, swapped in under the lock. */
/* LOCAL at the origin given, published without the lock. */
t_locks = t_unlocks = 0;
space = RETRO_VR_REFERENCE_SPACE_LOCAL;
CHECK(openxr_session_swap_track_space(&s, &space, &origin,
@@ -1690,10 +1700,15 @@ static void test_track_space(void)
t_space_pose.position.z);
CHECK(retro_atomic_load_acquire_int(&s.track_seq) == 1,
"seq %d after the first", retro_atomic_load_acquire_int(&s.track_seq));
CHECK(t_locks == 1 && t_unlocks == 1, "not swapped under the lock");
CHECK(!t_locks && !t_unlocks, "the swap took the lock");
CHECK(openxr_session_take_track(&s)
&& openxr_session_take_track(&s)->space == s.track_space
&& openxr_session_take_track(&s)->seq == 1,
"the frame does not see the first space");
first = s.track_space;
/* STAGE replaces it; the old one goes under the lock. */
/* STAGE replaces it; the old one goes at the frame's next take,
* never under a frame that may still submit in it. */
t_spaces_destroyed = 0;
space = RETRO_VR_REFERENCE_SPACE_STAGE;
CHECK(openxr_session_swap_track_space(&s, &space, &origin,
@@ -1705,9 +1720,11 @@ static void test_track_space(void)
&& t_near(t_space_pose.orientation.w, 1.0f),
"STAGE made away from the room's origin: z %.2f",
t_space_pose.position.z);
CHECK(t_spaces_destroyed == 1 && t_space_destroyed_last == first
&& t_space_destroy_in_lock,
"the old space: %u destroyed, in the lock %d", t_spaces_destroyed,
CHECK(!t_spaces_destroyed, "the swap destroyed the space a frame uses");
CHECK( openxr_session_take_track(&s)->space == s.track_space
&& t_spaces_destroyed == 1 && t_space_destroyed_last == first
&& !t_space_destroy_in_lock,
"the old space: %u destroyed, in a lock %d", t_spaces_destroyed,
(int)t_space_destroy_in_lock);
CHECK(retro_atomic_load_acquire_int(&s.track_seq) == 2, "seq after STAGE");
@@ -1725,6 +1742,7 @@ static void test_track_space(void)
CHECK(LOGGED("[OpenXR] No STAGE space: tracked content is seated.\n"),
"log: %s", t_log);
t_stage_missing = false;
openxr_session_take_track(&s);
/* No STAGE and no LOCAL: nothing changes, nothing is logged. */
first = s.track_space;
@@ -2135,7 +2153,8 @@ static void test_host(void)
"the views the core was given did not go on in the new space");
openxr_seq_read(&f.proj_seq, f.proj_words, &p, sizeof(p));
CHECK( p.active
&& p.track_seq == retro_atomic_load_acquire_int(&s.track_seq)
&& openxr_frame_track_shows((const openxr_track_t*)
retro_atomic_load_acquire_ptr(&s.track), p.track_seq)
&& t_near(p.pose[0].position.x, -0.032f),
"the projection shown did not go on in the new space");
flags = 0;
@@ -2404,7 +2423,9 @@ static void test_host_recenter_paused(void)
openxr_seq_read(&f.proj_seq, f.proj_words, &shown, sizeof(shown));
CHECK( openxr_host_has_given(&h) && p.track_seq == seq
&& t_near(p.pose[0].position.x, -0.25f)
&& shown.track_seq == seq && t_near(shown.pose[0].position.x, -0.25f),
&& openxr_frame_track_shows((const openxr_track_t*)
retro_atomic_load_acquire_ptr(&s.track), shown.track_seq)
&& t_near(shown.pose[0].position.x, -0.25f),
"the paused frame's views did not go on in the new space");
memset(&t_proj, 0, sizeof(t_proj));
openxr_frame_run(&f);
@@ -2506,9 +2527,9 @@ static void t_xr_frame(void)
t_xr_no_proj++;
}
/* A headset frame between a recenter's swap and its carry would show
* the new space without the projection: none gets in, running or
* paused. */
/* A headset frame while a recenter's swap is under way, or right after
* it, shows the projection: the old space with it, or the new one it
* carries to - running or paused. */
static void test_host_recenter_locked(void)
{
openxr_runtime_t rt;
@@ -2549,12 +2570,13 @@ static void test_host_recenter_locked(void)
t_xr = &f;
t_xr_frames = 0;
t_xr_no_proj = 0;
t_at_unlock = t_xr_frame;
t_at_remake = t_xr_frame;
if (pass)
openxr_host_place_kept(&h);
else
openxr_host_sample(&h, 0, &flags);
t_at_unlock = NULL;
t_at_remake = NULL;
t_xr_frame();
CHECK(retro_atomic_load_acquire_int(&s.track_seq) != seq,
"%s: no remake", pass ? "paused" : "running");
CHECK(t_xr_frames && !t_xr_no_proj,
@@ -4944,10 +4966,11 @@ static void test_screen_placement_seated(void)
t_xr = &f;
t_xr_frames = 0;
t_xr_off = 0;
t_at_unlock = t_xr_frame_screens;
t_at_remake = t_xr_frame_screens;
space = RETRO_VR_REFERENCE_SPACE_STAGE;
openxr_host_set_space(&h, &space, &w, &ht);
t_at_unlock = NULL;
t_at_remake = NULL;
t_xr_frame_screens();
t_stage_missing = false;
CHECK(space == RETRO_VR_REFERENCE_SPACE_LOCAL && t_xr_frames && !t_xr_off,
"no STAGE: %u of %u headset frames at the fallback's makes showed "
@@ -5522,6 +5545,25 @@ static void test_frame_thread_hooks(void)
}
}
static void t_carry_noted(void *user, int from, int to)
{
(void)user; (void)from; (void)to;
}
/* Swap and show a frame: whether the projection went out, in the track
* space the swap published. */
static bool t_carry_shown(openxr_frame_t *f, openxr_session_t *s)
{
memset(&t_proj, 0, sizeof(t_proj));
openxr_frame_run(f);
return t_last_layer_count == 1
&& t_layer_types[0] == XR_TYPE_COMPOSITION_LAYER_PROJECTION
&& t_proj.space == s->track_space;
}
/* A recenter's remake carries a projection drawn in the space before:
* read as the frame's track space allows, never rewritten, through
* consecutive carries, and not through a remake without one. */
static void test_frame_carried_seq(void)
{
static int binding;
@@ -5530,45 +5572,204 @@ static void test_frame_carried_seq(void)
openxr_swapchains_t sc;
openxr_frame_t f;
openxr_projection_t p, got;
XrPosef origin;
enum retro_vr_reference_space space = RETRO_VR_REFERENCE_SPACE_LOCAL;
unsigned n = 0;
int cb_user = 0;
int drawn;
t_frame_fresh(&rt, &s, &sc, &f);
memset(&origin, 0, sizeof(origin));
origin.orientation.w = 1.0f;
openxr_session_swap_track_space(&s, &space, &origin, NULL, NULL);
CHECK(openxr_slot_create(&sc, OPENXR_PROJECTION_SLOT, 44, false,
VIDEO_SCALE_PACK(64, 48), 2, t_images_cb, &cb_user, &n),
"projection slot");
retro_atomic_store_release_int(&sc.slots[OPENXR_PROJECTION_SLOT].content, 1);
openxr_session_set_alive(&s, true);
retro_atomic_store_release_int(&s.state, XR_SESSION_STATE_FOCUSED);
memset(&p, 0, sizeof(p));
p.active = true;
p.track_seq = 10;
p.width = 400;
p.height = 240;
p.width = 64;
p.height = 48;
p.pose[0].position.x = -0.03f;
p.pose[1].position.x = 0.03f;
p.fov[0].angleLeft = -0.75f;
p.fov[1].angleRight = 0.8f;
openxr_session_lock(&s);
p.track_seq = drawn = retro_atomic_load_acquire_int(&s.track_seq);
openxr_frame_publish_projection(&f, &p);
openxr_frame_move_projection(&f, 10, 11);
CHECK(openxr_frame_carried_seq(&f, 10) == 11, "first carry missing");
openxr_frame_move_projection(&f, 11, 12);
CHECK(openxr_frame_carried_seq(&f, 10) == 12
&& openxr_frame_carried_seq(&f, 11) == 12
&& openxr_frame_carried_seq(&f, 9) == 9,
"consecutive carries not composed");
CHECK(t_carry_shown(&f, &s), "drawn in the space: not shown");
/* Two recenters between frames: carried through both, as drawn. */
openxr_session_swap_track_space(&s, &space, &origin, t_carry_noted, NULL);
openxr_session_swap_track_space(&s, &space, &origin, t_carry_noted, NULL);
CHECK(t_carry_shown(&f, &s), "not carried through two recenters");
openxr_seq_read(&f.proj_seq, f.proj_words, &got, sizeof(got));
p.track_seq = 12;
CHECK(!memcmp(&p, &got, sizeof(p)), "carry changed projection poses or size");
openxr_frame_clear_projection(&f);
openxr_frame_move_projection(&f, 12, 13);
CHECK(openxr_frame_carried_seq(&f, 10) == 13,
"carry lost a queued projection while none was published");
openxr_frame_move_projection(&f, 20, 21);
CHECK(openxr_frame_carried_seq(&f, 20) == 21
&& openxr_frame_carried_seq(&f, 10) == 10,
"unrelated space was included in carry");
openxr_session_unlock(&s);
CHECK(!memcmp(&p, &got, sizeof(p)) && got.track_seq == drawn,
"the carry rewrote the projection: seq %d", got.track_seq);
/* A remake without a carry orphans it, and a carry after that does
* not reach back past the remake. */
openxr_session_swap_track_space(&s, &space, &origin, NULL, NULL);
CHECK(!t_carry_shown(&f, &s), "shown in a space remade without a carry");
openxr_session_swap_track_space(&s, &space, &origin, t_carry_noted, NULL);
CHECK(!t_carry_shown(&f, &s), "carried from before an uncarried remake");
p.track_seq = retro_atomic_load_acquire_int(&s.track_seq) - 1;
openxr_frame_publish_projection(&f, &p);
CHECK(t_carry_shown(&f, &s), "drawn in the remade space: not carried");
/* A new session starts with no carry. */
openxr_session_reset(&s);
openxr_session_create(&s, &binding);
openxr_frame_session_created(&f);
openxr_session_lock(&s);
CHECK(openxr_frame_carried_seq(&f, 20) == 20,
"new session kept a carry mapping");
openxr_session_unlock(&s);
CHECK(!s.carry_from && !s.carry_to, "a new session kept a carry");
openxr_session_swap_track_space(&s, &space, &origin, NULL, NULL);
CHECK(!t_carry_shown(&f, &s), "a carry outlived its session");
openxr_slot_destroy(&sc, OPENXR_PROJECTION_SLOT);
t_frame_free(&rt, &s, &f);
}
/* Spaces by the handle the threaded fakes give: 0x1000 + n. */
#define T_MT_SPACES 4096
static retro_atomic_int_t t_mt_made;
static retro_atomic_int_t t_mt_alive[T_MT_SPACES];
static retro_atomic_int_t t_mt_bad_destroys;
static retro_atomic_int_t t_mt_dead_submits;
static retro_atomic_int_t t_mt_no_proj;
static retro_atomic_int_t t_mt_frames;
static retro_atomic_int_t t_mt_done;
static openxr_session_t *t_mt_session;
static openxr_frame_t *t_mt_frame;
static XRAPI_ATTR XrResult XRAPI_CALL t_mt_create_space(XrSession s,
const XrReferenceSpaceCreateInfo *ci, XrSpace *out)
{
int n = retro_atomic_fetch_add_int(&t_mt_made, 1);
(void)s; (void)ci;
if (n >= T_MT_SPACES)
return XR_ERROR_LIMIT_REACHED;
retro_atomic_store_release_int(&t_mt_alive[n], 1);
*out = (XrSpace)(uintptr_t)(0x1000 + n);
return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL t_mt_destroy_space(XrSpace sp)
{
uintptr_t n = (uintptr_t)sp - 0x1000;
if ( n < T_MT_SPACES
&& !retro_atomic_exchange_int(&t_mt_alive[n], 0))
retro_atomic_fetch_add_int(&t_mt_bad_destroys, 1);
return XR_SUCCESS;
}
static XRAPI_ATTR XrResult XRAPI_CALL t_mt_end_frame(XrSession s,
const XrFrameEndInfo *ei)
{
(void)s;
retro_atomic_fetch_add_int(&t_mt_frames, 1);
if ( !ei->layerCount
|| ei->layers[0]->type != XR_TYPE_COMPOSITION_LAYER_PROJECTION)
retro_atomic_fetch_add_int(&t_mt_no_proj, 1);
else
{
uintptr_t n = (uintptr_t)((const XrCompositionLayerProjection*)
ei->layers[0])->space - 0x1000;
if (n >= T_MT_SPACES || !retro_atomic_load_acquire_int(&t_mt_alive[n]))
retro_atomic_fetch_add_int(&t_mt_dead_submits, 1);
}
return XR_SUCCESS;
}
static void t_mt_core(void *arg)
{
enum retro_vr_reference_space space = RETRO_VR_REFERENCE_SPACE_LOCAL;
XrPosef origin;
unsigned i;
(void)arg;
memset(&origin, 0, sizeof(origin));
origin.orientation.w = 1.0f;
for (i = 0; i < 2000; i++)
openxr_session_swap_track_space(t_mt_session, &space, &origin,
t_carry_noted, NULL);
retro_atomic_store_release_int(&t_mt_done, 1);
}
static void t_mt_xr(void *arg)
{
(void)arg;
while (!retro_atomic_load_acquire_int(&t_mt_done))
openxr_frame_run(t_mt_frame);
}
/* A core thread remaking its track space, each time with a carry, while
* the frame loop's thread submits: every frame shows the projection, in
* a space nothing has destroyed, and each space is destroyed once. */
static void test_track_swap_threads(void)
{
openxr_runtime_t rt;
openxr_session_t s;
openxr_swapchains_t sc;
openxr_frame_t f;
openxr_projection_t p;
XrPosef origin;
sthread_t *core, *xr;
enum retro_vr_reference_space space = RETRO_VR_REFERENCE_SPACE_LOCAL;
unsigned n = 0;
int cb_user = 0;
int alive = 0;
int i;
t_frame_fresh(&rt, &s, &sc, &f);
s.hooks.lock = NULL;
s.hooks.unlock = NULL;
s.CreateReferenceSpace = t_mt_create_space;
s.DestroySpace = t_mt_destroy_space;
f.EndFrame = t_mt_end_frame;
memset(&origin, 0, sizeof(origin));
origin.orientation.w = 1.0f;
openxr_session_swap_track_space(&s, &space, &origin, NULL, NULL);
CHECK(openxr_slot_create(&sc, OPENXR_PROJECTION_SLOT, 44, false,
VIDEO_SCALE_PACK(64, 48), 2, t_images_cb, &cb_user, &n),
"projection slot");
retro_atomic_store_release_int(&sc.slots[OPENXR_PROJECTION_SLOT].content, 1);
openxr_session_set_alive(&s, true);
retro_atomic_store_release_int(&s.state, XR_SESSION_STATE_FOCUSED);
memset(&p, 0, sizeof(p));
p.active = true;
p.width = 64;
p.height = 48;
p.track_seq = retro_atomic_load_acquire_int(&s.track_seq);
openxr_frame_publish_projection(&f, &p);
openxr_frame_run(&f);
t_mt_session = &s;
t_mt_frame = &f;
core = sthread_create(t_mt_core, NULL);
xr = sthread_create(t_mt_xr, NULL);
CHECK(core && xr, "threads");
if (core)
sthread_join(core);
if (xr)
sthread_join(xr);
openxr_frame_run(&f);
for (i = 0; i < T_MT_SPACES; i++)
alive += retro_atomic_load_acquire_int(&t_mt_alive[i]);
CHECK(retro_atomic_load_acquire_int(&t_mt_frames) > 1,
"the frame loop ran no frames beside the swaps");
CHECK(!retro_atomic_load_acquire_int(&t_mt_dead_submits),
"%d frames submitted in a destroyed space",
retro_atomic_load_acquire_int(&t_mt_dead_submits));
CHECK(!retro_atomic_load_acquire_int(&t_mt_no_proj),
"%d of %d frames left the carried projection out",
retro_atomic_load_acquire_int(&t_mt_no_proj),
retro_atomic_load_acquire_int(&t_mt_frames));
CHECK(!retro_atomic_load_acquire_int(&t_mt_bad_destroys),
"%d spaces destroyed twice",
retro_atomic_load_acquire_int(&t_mt_bad_destroys));
CHECK(alive == 1, "%d spaces left of %d made, want the current one",
alive, retro_atomic_load_acquire_int(&t_mt_made));
openxr_slot_destroy(&sc, OPENXR_PROJECTION_SLOT);
t_frame_free(&rt, &s, &f);
}
@@ -6531,10 +6732,9 @@ static void test_gl_host_yield(void)
t_gl_books_closed("yield");
}
static openxr_frame_t *t_gl_carry_frame;
static void t_gl_carry(void *user, int from, int to)
{
openxr_frame_move_projection(t_gl_carry_frame, from, to);
(void)user; (void)from; (void)to;
}
static void test_gl_host_projection(void)
@@ -6601,7 +6801,6 @@ static void test_gl_host_projection(void)
t_sc_wait = XR_TIMEOUT_EXPIRED;
p.pose[0].position.x = -0.07f;
CHECK(t_gl_draw(xr, slot, dims, 2, 4, &p) && t_gate_step(1), "queued");
t_gl_carry_frame = &xr->frame;
CHECK(openxr_session_swap_track_space(&xr->session, &space, &origin,
t_gl_carry, NULL)
&& openxr_session_swap_track_space(&xr->session, &space, &origin,
@@ -6610,10 +6809,11 @@ static void test_gl_host_projection(void)
CHECK(t_gate_step(1), "frame");
openxr_seq_read(&xr->frame.proj_seq, xr->frame.proj_words, &got,
sizeof(got));
CHECK( got.track_seq == seq + 2 && got.pose[0].position.x == -0.07f
CHECK( got.track_seq == seq && got.pose[0].position.x == -0.07f
&& t_layer_types[0] == XR_TYPE_COMPOSITION_LAYER_PROJECTION
&& t_proj.space == xr->session.track_space
&& t_proj_views[0].pose.position.x == -0.07f,
"carried: seq %d of %d, x %f", got.track_seq, seq + 2,
"carried: seq %d drawn in %d, x %f", got.track_seq, seq,
t_proj_views[0].pose.position.x);
/* Another eye size, its image not copied yet: the old images show,
@@ -6681,6 +6881,7 @@ int main(void)
test_frame();
test_frame_thread_hooks();
test_frame_carried_seq();
test_track_swap_threads();
test_frame_pacing();
test_frame_lost();
test_track_space();