From 9080a67663ca1687687de8da849a8db0cbfdafd4 Mon Sep 17 00:00:00 2001 From: davey Date: Thu, 8 Oct 2026 03:21:40 -0700 Subject: [PATCH] video_thread: the headset calls reach the driver under threaded video The wrapper's tables left the headset refresh query and the three VR calls empty. With Threaded Video on, the menu offered fallback rates, the headset was never asked for its rate, and on the Quest a VR core saw no headset, had its stereo map refused and got no poses. The refresh and the views status are answered on the caller's thread: the drivers read only what the headset thread publishes and what init and free change. The stereo request and the poses run on the video thread, as do the Quest context's sample, reference-space change and stereo flag, after the frames already queued: the frame locates its views in that space and is submitted with the poses it was drawn with, so tracked content runs in step with the video thread. Headset pacing stays off under threaded video, as before: the tick wait is in the driver's frame, which does not hold the core there. --- gfx/video_driver.c | 63 +++++- gfx/video_thread_wrapper.c | 97 ++++++++- retroarch.c | 7 +- .../gfx/threaded_video/threaded_video_test.c | 192 ++++++++++++++++++ 4 files changed, 346 insertions(+), 13 deletions(-) diff --git a/gfx/video_driver.c b/gfx/video_driver.c index 00340eccc701..69e98db914cb 100644 --- a/gfx/video_driver.c +++ b/gfx/video_driver.c @@ -9127,45 +9127,84 @@ static int video_driver_vr_ctx(void) return vr_ctx_id; } -void video_driver_vr_content_clear(void) +/* The headset context's state belongs to the thread drawing its frames: + * under the threaded wrapper the video thread, so it runs there, after + * the frames already handed over. Those keep the poses, space and + * stereo state they were made with, and a sample predicts from the + * last one's display time. */ +static uintptr_t video_driver_vr_ctx_run(uintptr_t (*func)(void*), + void *data) { +#ifdef HAVE_THREADS + if (video_driver_thread_wrapper_active()) + { + video_thread_wait_idle(); + return video_thread_run_blocking(func, data); + } +#endif + return func(data); +} + #if defined(ANDROID) +static uintptr_t video_driver_vr_content_clear_cb(void *data) +{ + (void)data; gl_android_openxr_set_stereo(false); + return 0; +} +#endif + +void video_driver_vr_content_clear(void) +{ +#if defined(ANDROID) + video_driver_vr_ctx_run(video_driver_vr_content_clear_cb, NULL); #endif } -/* Called by the runloop once per iteration, BEFORE retro_run(), so the - * pose a core reads is the one the frame will be rendered with. */ -bool video_driver_vr_sample_tracking(void) +static uintptr_t video_driver_vr_sample(void *data) { - bool ok = false; - uint32_t flags = 0; + uint32_t *flags = (uint32_t*)data; + bool ok = false; switch (video_driver_vr_ctx()) { #if defined(ANDROID) && defined(HAVE_VULKAN) case VR_CTX_VK: ok = android_vk_openxr_sample_tracking(); - flags = android_vk_openxr_take_frame_flags(); + *flags = android_vk_openxr_take_frame_flags(); break; #endif #if defined(ANDROID) case VR_CTX_GL: ok = gl_android_openxr_sample_tracking(); - flags = gl_android_openxr_take_frame_flags(); + *flags = gl_android_openxr_take_frame_flags(); break; #endif default: break; } + return ok; +} + +/* Called by the runloop once per iteration, BEFORE retro_run(), so the + * pose a core reads is the one the frame will be rendered with. */ +bool video_driver_vr_sample_tracking(void) +{ + bool ok = false; + uint32_t flags = 0; + + if (video_driver_vr_ctx() != VR_CTX_NONE) + ok = video_driver_vr_ctx_run(video_driver_vr_sample, &flags) != 0; + video_vr_frame_flags = flags; return ok; } -static bool video_driver_set_vr_content_info(video_vr_content_info_t *info) +static uintptr_t video_driver_vr_content_info_apply(void *data) { + video_vr_content_info_t *info = (video_vr_content_info_t*)data; video_driver_state_t *video_st = video_state_get_ptr(); unsigned w = 0, h = 0; @@ -9233,6 +9272,12 @@ static bool video_driver_set_vr_content_info(video_vr_content_info_t *info) return true; } +static bool video_driver_set_vr_content_info(video_vr_content_info_t *info) +{ + return video_driver_vr_ctx_run(video_driver_vr_content_info_apply, + info) != 0; +} + bool video_driver_get_vr_head_pose(struct retro_vr_head_pose *out) { switch (video_driver_vr_ctx()) diff --git a/gfx/video_thread_wrapper.c b/gfx/video_thread_wrapper.c index 47a908a1ec84..4f5b01ca01db 100644 --- a/gfx/video_thread_wrapper.c +++ b/gfx/video_thread_wrapper.c @@ -4294,6 +4294,21 @@ static void thread_hw_context_destroying(void *data) } } +/* Direct, as the runloop asks every iteration: the driver answers from + * what its headset thread publishes and from what only init and free + * change, which this thread waits out. */ +static float thread_get_headset_refresh(void *data, float *rates, + unsigned cap, unsigned *count) +{ + thread_video_t *thr = (thread_video_t*)data; + + if ( thr && thr->driver_data + && thr->poke && thr->poke->get_headset_refresh) + return thr->poke->get_headset_refresh(thr->driver_data, rates, cap, + count); + return 0.0f; +} + static void thread_set_texture_frame(void *data, const void *frame, bool rgb32, unsigned dims, float alpha) { @@ -4664,7 +4679,8 @@ static const video_poke_interface_t thread_poke = { NULL, /* texture_lend_ready */ NULL, /* get_last_present_wait: read on the video thread */ thread_set_view_count, - thread_hw_context_destroying + thread_hw_context_destroying, + thread_get_headset_refresh }; /* Video thread, for video_thread_get_poke_interface(): installs the @@ -4719,6 +4735,76 @@ static bool video_thread_wrapper_gfx_widgets_enabled(void *data) } #endif +#ifdef HAVE_OPENXR +typedef struct +{ + thread_video_t *thr; + const video_vr_content_info_t *info; + struct retro_vr_frame_state *frame; +} video_thread_vr_call_t; + +static uintptr_t video_thread_set_vr_content_info_cb(void *data) +{ + video_thread_vr_call_t *call = (video_thread_vr_call_t*)data; + return call->thr->driver->set_vr_content_info(call->thr->driver_data, + call->info); +} + +/* On the video thread: the stereo state it sets is the frame's. */ +static bool video_thread_set_vr_content_info(void *data, + const video_vr_content_info_t *info) +{ + video_thread_vr_call_t call; + thread_video_t *thr = (thread_video_t*)data; + + if ( !thr || !thr->driver_data || !thr->driver + || !thr->driver->set_vr_content_info) + return false; + call.thr = thr; + call.info = info; + call.frame = NULL; + return video_thread_run_blocking(video_thread_set_vr_content_info_cb, + &call) != 0; +} + +static uintptr_t video_thread_get_vr_frame_state_cb(void *data) +{ + video_thread_vr_call_t *call = (video_thread_vr_call_t*)data; + return call->thr->driver->get_vr_frame_state(call->thr->driver_data, + call->frame); +} + +/* On the video thread: taking the poses marks them for the frame that + * submits them. */ +static bool video_thread_get_vr_frame_state(void *data, + struct retro_vr_frame_state *out) +{ + video_thread_vr_call_t call; + thread_video_t *thr = (thread_video_t*)data; + + if ( !thr || !thr->driver_data || !thr->driver + || !thr->driver->get_vr_frame_state) + return false; + call.thr = thr; + call.info = NULL; + call.frame = out; + return video_thread_run_blocking(video_thread_get_vr_frame_state_cb, + &call) != 0; +} + +/* Direct, as cores ask every frame: the drivers answer from the session + * init made, which only free takes down. */ +static unsigned video_thread_get_video_views_status(void *data) +{ + thread_video_t *thr = (thread_video_t*)data; + + if ( thr && thr->driver_data && thr->driver + && thr->driver->get_video_views_status) + return thr->driver->get_video_views_status(thr->driver_data); + return 0; +} +#endif + static const video_driver_t video_thread = { video_thread_init_never_call, /* Should never be called directly. */ video_thread_frame, @@ -4755,7 +4841,14 @@ static const video_driver_t video_thread = { /* Without this the runloop's invalidate stopped at the wrapper: the * driver kept the core's image across a reset, and the ring kept * handing it back across a close. */ - video_thread_invalidate_hw_render_cache + video_thread_invalidate_hw_render_cache, + NULL, /* read_viewport_hdr */ + NULL, /* font_backend: CMD_INIT/CMD_FREE read the driver's */ +#ifdef HAVE_OPENXR + video_thread_get_vr_frame_state, + video_thread_set_vr_content_info, + video_thread_get_video_views_status, +#endif }; static void video_thread_set_callbacks(thread_video_t *thr, diff --git a/retroarch.c b/retroarch.c index 6868be3769b3..d3114e4c5599 100644 --- a/retroarch.c +++ b/retroarch.c @@ -1538,9 +1538,12 @@ static void driver_adjust_system_rates( #ifdef HAVE_OPENXR /* A headset whose rate fits the core paces it in the window's place: - * its rate and interval stand in for the display's. */ + * its rate and interval stand in for the display's. Not under + * threaded video, where the driver's frame, which waits for the + * headset, does not hold the core. */ video_st->headset_vsync = settings->bools.video_vsync; - video_st->headset_interval = video_st->headset_vsync ? headset_fit : 0; + video_st->headset_interval = (video_st->headset_vsync + && !video_driver_thread_wrapper_active()) ? headset_fit : 0; if (video_st->headset_interval) { video_refresh_rate = video_st->headset_hz; diff --git a/samples/gfx/threaded_video/threaded_video_test.c b/samples/gfx/threaded_video/threaded_video_test.c index 4ea79c8f53d6..782e4e8491ba 100644 --- a/samples/gfx/threaded_video/threaded_video_test.c +++ b/samples/gfx/threaded_video/threaded_video_test.c @@ -982,6 +982,197 @@ static void lane_context_api_behind_wrapper(void) (int)api); } +/* ------------------------------------------------------------------ */ +/* Lane: headset calls behind the wrapper */ +/* The headset's rates, its status, a stereo request and the eye */ +/* poses go to the wrapped driver, and its answers come back. The */ +/* request and the poses are run on the video thread, whose frame */ +/* reads what they change. Without the forwards the menu offered */ +/* the fallback rates, the runloop never learned the headset's */ +/* rate, and a VR core saw no headset at all. A rate that fits */ +/* still leaves threaded video on the window's pacing. */ +/* ------------------------------------------------------------------ */ + +static video_driver_t hslane_driver; +static const video_driver_t *hslane_inner; +static video_poke_interface_t hslane_poke; +static const video_poke_interface_t *hslane_inner_poke; +#ifdef HAVE_OPENXR +static unsigned hslane_info_calls; +static uintptr_t hslane_info_thread; +static bool hslane_info_stereo; +static unsigned hslane_pose_calls; +static uintptr_t hslane_pose_thread; +#endif + +/* Out of order, and fewer than any fallback list. Twice the harness + * core's 60 fps, so the headset would pace it. */ +static float hslane_refresh(void *data, float *rates, unsigned cap, + unsigned *count) +{ + (void)data; + *count = 0; + if (cap >= 2) + { + rates[0] = 120.0f; + rates[1] = 80.0f; + *count = 2; + } + return 120.0f; +} + +static void hslane_get_poke(void *data, const video_poke_interface_t **iface) +{ + hslane_inner->poke_interface(data, &hslane_inner_poke); + hslane_poke = *hslane_inner_poke; + hslane_poke.get_headset_refresh = hslane_refresh; + *iface = &hslane_poke; +} + +#ifdef HAVE_OPENXR +static unsigned hslane_status(void *data) +{ + (void)data; + return RETRO_VIDEO_VIEWS_STATUS_HMD; +} + +static bool hslane_set_info(void *data, const video_vr_content_info_t *info) +{ + (void)data; + hslane_info_calls++; + hslane_info_thread = sthread_get_current_thread_id(); + hslane_info_stereo = info && info->stereo_native; + return true; +} + +static bool hslane_frame_state(void *data, struct retro_vr_frame_state *out) +{ + (void)data; + hslane_pose_calls++; + hslane_pose_thread = sthread_get_current_thread_id(); + memset(out->eyes, 0, sizeof(out->eyes)); + out->eyes[0].position[0] = -0.032f; + out->eyes[1].position[0] = 0.032f; + return true; +} +#endif + +static void lane_headset_calls(void) +{ + unsigned had = failures; + video_driver_state_t *video_st = video_state_get_ptr(); + thread_video_t *thr; + unsigned values[8]; + float rates[4]; + unsigned n; + unsigned count = 0; + float hz = 0.0f; + + set_threaded_via_setting(true); + run_frames(4); + expect_wrapper(true, "headset lane"); + if (!(thr = (thread_video_t*)video_st->data)) + return; + + video_thread_wait_idle(); + hslane_inner = thr->driver; + hslane_driver = *thr->driver; + hslane_driver.poke_interface = hslane_get_poke; +#ifdef HAVE_OPENXR + hslane_driver.get_video_views_status = hslane_status; + hslane_driver.set_vr_content_info = hslane_set_info; + hslane_driver.get_vr_frame_state = hslane_frame_state; +#endif + set_driver(thr, &hslane_driver); + set_poke_from(thr, &hslane_driver); + + /* The menu's list: the two fixed choices, then the driver's rates + * in order, not the list offered while no headset answers. */ + n = video_driver_headset_rate_choices(values, 8); + CHECK(n == 4 && values[2] == 80 && values[3] == 120, + "headset lane: %u rate choices (%u, %u, ...), wanted the " + "driver's 80 and 120", n, n > 2 ? values[2] : 0, + n > 3 ? values[3] : 0); + if (video_st->poke && video_st->poke->get_headset_refresh) + hz = video_st->poke->get_headset_refresh(video_st->data, rates, 4, + &count); + CHECK(hz == 120.0f && count == 2, + "headset lane: the wrapper's poke answered %.1f Hz with %u rates", + (double)hz, count); + +#ifdef HAVE_OPENXR + /* The runloop's poll, every iteration */ + run_frames(2); + CHECK(video_st->headset_hz == 120.0f, + "headset lane: the runloop took the headset's rate as %.1f Hz", + (double)video_st->headset_hz); + + /* The rate fits, and vsync is what lets a headset pace, but threaded + * video stays on the window's pacing. */ + { + settings_t *settings = config_get_ptr(); + settings->bools.video_vsync = true; + run_frames(2); + CHECK(video_st->headset_interval == 0, + "headset lane: the headset paced threaded video (interval %u)", + video_st->headset_interval); + settings->bools.video_vsync = false; + run_frames(2); + } + + CHECK(video_driver_views_status() & RETRO_VIDEO_VIEWS_STATUS_HMD, + "headset lane: the views status did not carry the driver's HMD"); + + { + video_vr_content_info_t info; + bool ok = false; + memset(&info, 0, sizeof(info)); + info.stereo_native = true; + hslane_info_calls = 0; + if (video_st->current_video->set_vr_content_info) + ok = video_st->current_video->set_vr_content_info(video_st->data, + &info); + CHECK(ok && hslane_info_calls == 1 && hslane_info_stereo, + "headset lane: a stereo request reached the driver %u times " + "(answer %d)", hslane_info_calls, ok); + CHECK(!hslane_info_calls + || hslane_info_thread == sthread_get_thread_id(thr->thread), + "headset lane: the stereo request ran off the video thread"); + } + + { + struct retro_vr_frame_state fs; + bool ok; + memset(&fs, 0, sizeof(fs)); + hslane_pose_calls = 0; + ok = video_driver_get_vr_frame_state(&fs); + CHECK(ok && hslane_pose_calls == 1 && fs.eyes[1].position[0] == 0.032f, + "headset lane: the eye poses reached the driver %u times " + "(answer %d)", hslane_pose_calls, ok); + CHECK(!hslane_pose_calls + || hslane_pose_thread == sthread_get_thread_id(thr->thread), + "headset lane: the eye poses were read off the video thread"); + } +#endif + + video_thread_wait_idle(); + set_driver(thr, hslane_inner); + set_poke(thr, hslane_inner_poke); +#ifdef HAVE_OPENXR + /* No headset again: the poll puts the configured rate back. */ + run_frames(2); + CHECK(video_st->headset_hz == 0.0f, + "headset lane: the headset's rate stayed at %.1f Hz", + (double)video_st->headset_hz); +#endif + run_frames(2); + set_threaded_via_setting(false); + run_frames(2); + + if (failures == had) + fprintf(stderr, "[pass] headset calls lane\n"); +} + /* ------------------------------------------------------------------ */ /* Lane: presenter repeats */ /* With video_threaded_present_repeat on and a driver that can */ @@ -7192,6 +7383,7 @@ int main(int argc, char *argv[]) lane_stat_text_bounds(); lane_viewport_publish(); lane_context_api_behind_wrapper(); + lane_headset_calls(); lane_async_texture_load(); if (!real_driver()) lane_present_repeat();