diff --git a/runtime/include/psx_openxr.h b/runtime/include/psx_openxr.h index 8a949c3f..f0e324c8 100644 --- a/runtime/include/psx_openxr.h +++ b/runtime/include/psx_openxr.h @@ -23,6 +23,7 @@ typedef struct PSXOpenXRStats { double quad_distance_m, quad_width_m, quad_height_m; /* last submitted quad */ uint32_t submitted_source; /* 1 fresh stereo pair, 2 native presentation */ uint64_t native_submitted, submitted_native_frame; + uint64_t losses; /* session/instance/swapchain losses torn down and retried */ } PSXOpenXRStats; int psx_openxr_enable(int enabled); int psx_openxr_begin(int width,int height,double units); diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 5153486b..99d52013 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -8154,7 +8154,7 @@ static void handle_openxr_stats(int id, const char *json) { "\"gl_version\":%llu,\"min_gl_version\":%llu,\"max_gl_version\":%llu," "\"submitted_layer\":%u,\"quad_submitted\":%llu," "\"submitted_source\":%u,\"native_submitted\":%llu,\"submitted_native_frame\":%llu," - "\"quad_distance_m\":%.6f,\"quad_width_m\":%.6f,\"quad_height_m\":%.6f}", id,s.compiled,s.enabled, + "\"quad_distance_m\":%.6f,\"quad_width_m\":%.6f,\"quad_height_m\":%.6f,\"losses\":%llu}", id,s.compiled,s.enabled, s.initialized,s.running,s.state,s.tracking,s.frame_open,s.result, s.stage?s.stage:"off",s.runtime,s.last_failure?s.last_failure:"",s.last_failure_result, (unsigned long long)s.view_flags,(unsigned long long)s.waits, @@ -8164,7 +8164,7 @@ static void handle_openxr_stats(int id, const char *json) { (unsigned long long)s.max_gl_version,s.submitted_layer, (unsigned long long)s.quad_submitted,s.submitted_source, (unsigned long long)s.native_submitted,(unsigned long long)s.submitted_native_frame, - s.quad_distance_m,s.quad_width_m,s.quad_height_m); + s.quad_distance_m,s.quad_width_m,s.quad_height_m,(unsigned long long)s.losses); } static void handle_openxr_views(int id,const char *json) { PSXOpenXRStats s; (void)json;psx_openxr_stats(&s); diff --git a/runtime/src/psx_openxr.c b/runtime/src/psx_openxr.c index 4bc68210..4ca339a2 100644 --- a/runtime/src/psx_openxr.c +++ b/runtime/src/psx_openxr.c @@ -1,5 +1,11 @@ /* Opt-in Win32 OpenXR lifecycle. No guest state, console diagnostics or timing - * phases: located poses and projection submission belong to one host frame. */ + * phases: located poses and projection submission belong to one host frame. + * + * THREADING: every entry point runs on the emulation/main thread that owns the + * GL context. Plugin hooks (psx_mod_openxr_*), the GL present path + * (gl_swap_with_osd) and TCP handlers (executed at the debug_server_poll safe + * point; the debug I/O thread only queues requests) are all that thread, and + * no other thread creates a GL context or touches XR. Hence no locking. */ #include "psx_openxr.h" #include "vr_pose_math.h" #include @@ -100,9 +106,22 @@ static XrView s_views[2]; static PSXModRenderView s_render[2]; static double s_oq[4],s_op[3]; static XrTime s_time; +/* Loss handling. A lost session/instance (or an unusable swapchain) is only + * flagged where it is detected; service_loss() tears the stack down at the end + * of the public call so no caller loses handles mid-sequence. */ +#define XR_RETRY_MS 3000u +static int s_reset_pending,s_exit_pending,s_recovering; +static const char *s_reset_stage; +static uint64_t s_retry_ms; static int check(XrResult r,const char *stage) { s_stats.result=r; - if(XR_FAILED(r)) { s_stats.stage=s_stats.last_failure=stage;s_stats.last_failure_result=r;s_stats.failures++;return 0; } + if(XR_FAILED(r)) { + s_stats.stage=s_stats.last_failure=stage;s_stats.last_failure_result=r;s_stats.failures++; + if(r==XR_ERROR_SESSION_LOST || r==XR_ERROR_INSTANCE_LOST) { + s_reset_pending=1;s_reset_stage=r==XR_ERROR_SESSION_LOST?"session_lost":"instance_lost"; + } + return 0; + } return 1; } static int input_initialize(void) { @@ -257,23 +276,37 @@ static int events(void) { } else if(e->state==XR_SESSION_STATE_STOPPING) { if(!check(xrEndSession(s_session),"end_session"))return 0; s_stats.running=0; - } else if(e->state==XR_SESSION_STATE_LOSS_PENDING || e->state==XR_SESSION_STATE_EXITING) { - s_stats.enabled=0;s_stats.running=0;s_stats.stage="session_exit";return 0; + } else if(e->state==XR_SESSION_STATE_LOSS_PENDING) { + s_reset_pending=1;s_reset_stage="session_loss";return 0; + } else if(e->state==XR_SESSION_STATE_EXITING) { + s_exit_pending=1;return 0; } } else if(event.type==XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING) { /* Recenter from the next valid located pair. */ const XrEventDataReferenceSpaceChangePending *e=(const void*)&event; if(e->referenceSpaceType==XR_REFERENCE_SPACE_TYPE_LOCAL) s_origin_reset_time=e->changeTime; } else if(event.type==XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING) { - s_stats.enabled=0;s_stats.running=0;s_stats.stage="instance_loss";return 0; + s_reset_pending=1;s_reset_stage="instance_loss";return 0; } } return 1; } -static void hands_locate(void) { +/* Sync the gameplay action set. Needed by both the input poll and hand + * location (a pose action is only active after a sync), so a plugin that only + * calls psx_mod_openxr_begin still gets hands. True only for XR_SUCCESS: the + * session is focused and the actions are current. */ +static int sync_actions(void) { + XrActiveActionSet active={s_actions,XR_NULL_PATH}; + XrActionsSyncInfo sync={XR_TYPE_ACTIONS_SYNC_INFO};sync.countActiveActionSets=1;sync.activeActionSets=&active; + XrResult r=xrSyncActions(s_session,&sync); + return check(r,"sync_actions") && r==XR_SUCCESS; +} +/* Focus for hands comes from the session state events and a successful action + * sync in this call, never from whether the plugin polled psx_mod_openxr_input. */ +static void hands_locate(int focused) { PSXModOpenXRHands current=s_hands; current.predicted_time=(uint64_t)s_time; - current.focused=s_stats.state==XR_SESSION_STATE_FOCUSED && s_input.focused; + current.focused=focused && s_stats.state==XR_SESSION_STATE_FOCUSED; current.origin_valid=s_origin_valid; memcpy(current.origin_position_m,s_op,sizeof s_op); memcpy(current.origin_orientation_xyzw,s_oq,sizeof s_oq); @@ -300,6 +333,48 @@ static void hands_locate(void) { } s_hands=current;s_hands_sample_ms=psx_host_mono_ms(); } +/* Destroy every XR object (children before parents). Safe on a partially + * initialised stack and on a lost session: destroy calls need no live session. */ +static void xr_destroy(void) { + for(int eye=0;eye<2;eye++) { + for(int k=0;k<2;k++) { + if(s_pose_space[eye][k])xrDestroySpace(s_pose_space[eye][k]); + s_pose_space[eye][k]=XR_NULL_HANDLE; + } + if(s_chain[eye])xrDestroySwapchain(s_chain[eye]); + s_chain[eye]=XR_NULL_HANDLE;free(s_images[eye]);s_images[eye]=NULL;s_count[eye]=0; + } + if(s_space)xrDestroySpace(s_space);s_space=XR_NULL_HANDLE; + if(s_view_space)xrDestroySpace(s_view_space);s_view_space=XR_NULL_HANDLE; + if(s_session)xrDestroySession(s_session);s_session=XR_NULL_HANDLE; + if(s_actions)xrDestroyActionSet(s_actions);s_actions=XR_NULL_HANDLE;s_stick=XR_NULL_HANDLE; + s_trigger=s_squeeze=XR_NULL_HANDLE;memset(s_click,0,sizeof s_click); + memset(s_pose_action,0,sizeof s_pose_action); + if(s_instance)xrDestroyInstance(s_instance);s_instance=XR_NULL_HANDLE; + s_origin_valid=0;s_origin_reset_time=0;s_stats.state=0; + s_stats.initialized=s_stats.running=s_stats.tracking=s_stats.frame_open=0; +} +/* Act on a flagged loss. A lost session/instance or an unusable swapchain tears + * everything down so the caller falls back to the flat present, then keeps XR + * enabled and re-initialises after XR_RETRY_MS. EXITING (the user closed the + * headset app) tears down and leaves XR disabled until an explicit enable. + * Returns 1 if it tore the stack down. Call only at the end of a public entry. */ +static int service_loss(void) { + if(!s_reset_pending && !s_exit_pending)return 0; + const int exiting=s_exit_pending && !s_reset_pending; + xr_destroy(); /* a lost session cannot end its frame; xr_destroy clears frame_open */ + memset(&s_input,0,sizeof s_input);s_input.struct_size=sizeof s_input; + hands_clear(); + s_quad_distance=s_quad_width=s_quad_height=0; + if(exiting) { + s_stats.enabled=0;s_stats.stage="session_exit";s_recovering=0;s_retry_ms=0; + } else { + s_stats.losses++;s_stats.stage=s_reset_stage?s_reset_stage:"lost"; + s_recovering=1;s_retry_ms=psx_host_mono_ms()+XR_RETRY_MS; + } + s_reset_pending=s_exit_pending=0;s_reset_stage=NULL; + return 1; +} #endif int psx_openxr_hands(PSXModOpenXRHands *out) { if(!out || out->struct_size!=sizeof *out)return 0; @@ -332,22 +407,8 @@ void psx_openxr_recenter(void) { void psx_openxr_shutdown(void) { #if defined(PSX_OPENXR) if(s_stats.frame_open)psx_openxr_end(0,NULL); - for(int eye=0;eye<2;eye++) { - for(int k=0;k<2;k++) { - if(s_pose_space[eye][k])xrDestroySpace(s_pose_space[eye][k]); - s_pose_space[eye][k]=XR_NULL_HANDLE; - } - if(s_chain[eye])xrDestroySwapchain(s_chain[eye]); - s_chain[eye]=XR_NULL_HANDLE;free(s_images[eye]);s_images[eye]=NULL;s_count[eye]=0; - } - if(s_space)xrDestroySpace(s_space);s_space=XR_NULL_HANDLE; - if(s_view_space)xrDestroySpace(s_view_space);s_view_space=XR_NULL_HANDLE; - if(s_session)xrDestroySession(s_session);s_session=XR_NULL_HANDLE; - if(s_actions)xrDestroyActionSet(s_actions);s_actions=XR_NULL_HANDLE;s_stick=XR_NULL_HANDLE; - s_trigger=s_squeeze=XR_NULL_HANDLE;memset(s_click,0,sizeof s_click); - memset(s_pose_action,0,sizeof s_pose_action); - if(s_instance)xrDestroyInstance(s_instance);s_instance=XR_NULL_HANDLE; - s_origin_valid=0;s_origin_reset_time=0;s_stats.initialized=s_stats.running=s_stats.tracking=s_stats.frame_open=0; + xr_destroy(); + s_reset_pending=s_exit_pending=s_recovering=0;s_reset_stage=NULL;s_retry_ms=0; #endif s_stats.enabled=0; s_quad_distance=s_quad_width=s_quad_height=0; @@ -392,10 +453,7 @@ int psx_openxr_input(PSXModOpenXRInput *out) { #endif #if defined(PSX_OPENXR) if(s_stats.enabled && s_stats.initialized && !s_stats.frame_open && events() && s_stats.running) { - XrActiveActionSet active={s_actions,XR_NULL_PATH}; - XrActionsSyncInfo sync={XR_TYPE_ACTIONS_SYNC_INFO};sync.countActiveActionSets=1;sync.activeActionSets=&active; - XrResult r=xrSyncActions(s_session,&sync); - if(check(r,"sync_actions") && r==XR_SUCCESS && s_stats.state==XR_SESSION_STATE_FOCUSED) { + if(sync_actions() && s_stats.state==XR_SESSION_STATE_FOCUSED) { PSXModOpenXRInput current=s_input;current.focused=1;int valid=1; for(int e=0;e<2;e++) { XrActionStateGetInfo get={XR_TYPE_ACTION_STATE_GET_INFO};get.action=s_stick;get.subactionPath=s_hand[e]; @@ -430,6 +488,8 @@ int psx_openxr_input(PSXModOpenXRInput *out) { if(valid)s_input=current; } } + /* events()/sync may have flagged a loss: tear down, return neutral input. */ + (void)service_loss(); #endif if(!s_input.focused)hands_clear(); *out=s_input;return 1; /* Always a fresh neutral sample when unavailable. */ @@ -441,17 +501,30 @@ int psx_openxr_begin(int width,int height,double units) { s_pair_id=s_pair_cycle=s_native_frame=0;s_source=0; hands_clear(); s_stats.units_per_meter=units; - if(!s_stats.initialized && !initialize()) { - /* Retain the failed producer/result. Avoid repeated costly startup until - * explicit re-enable; a disconnected headset is an inspectable state. */ - psx_openxr_shutdown();return 0; + if(!s_stats.initialized) { + if(s_retry_ms) { /* backing off after a loss: flat until the retry time */ + if(psx_host_mono_ms()=s_count[eye] || !copy(eye,s_images[eye][index].image,s_w[eye],s_h[eye])) { s_stats.stage="copy_image";s_stats.failures++;complete=0; } XrSwapchainImageReleaseInfo ri={XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; if(!check(xrReleaseSwapchainImage(s_chain[eye],&ri),"release"))complete=0; - } else complete=0; + } else { + /* Acquired but never writable. Release it best-effort so the acquire + * count cannot leak; a runtime that refuses (the wait never completed) + * leaves this swapchain unusable for the next acquire, so rebuild the + * whole stack rather than fail every later frame. */ + XrSwapchainImageReleaseInfo ri={XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; + (void)xrReleaseSwapchainImage(s_chain[eye],&ri); + if(wr==XR_TIMEOUT_EXPIRED){s_stats.stage=s_stats.last_failure="wait_image_timeout";s_stats.failures++;} + if(!s_reset_pending){s_reset_pending=1;s_reset_stage="swapchain_wait_failed";} + complete=0; + } pv[eye].pose=s_views[eye].pose;pv[eye].fov=s_views[eye].fov; pv[eye].subImage.swapchain=s_chain[eye];pv[eye].subImage.imageRect.extent.width=s_w[eye]; pv[eye].subImage.imageRect.extent.height=s_h[eye]; @@ -566,6 +650,7 @@ int psx_openxr_end(int keep,PSXOpenXRCopy copy) { } else s_stats.empty++; if(ok && complete)s_stats.stage="submitted"; s_quad_distance=s_quad_width=s_quad_height=0; + (void)service_loss(); /* after the frame is closed: lost session / bad swapchain */ return ok && complete; #else return 0;