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();