#include "xr_session.h" #include #include #include #include #include #include #include #define XR_USE_PLATFORM_ANDROID 1 #define XR_USE_GRAPHICS_API_OPENGL_ES 1 #include #include #include #include "xr_input.h" #include "xr_overlay.h" #include "xr_swapchain.h" #define TAG "QuestShock" #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) // Fixed pose/size for the single game quad - 2m in front of the local space // origin, sized to preserve the game's own aspect ratio at a comfortable // visual angle. No per-eye view/projection work is needed for a quad layer // at all - the compositor handles reprojecting it per eye itself, which is // exactly why this milestone doesn't need xrLocateViews or any stereo // rendering (see the plan's step A notes). #define QUAD_DISTANCE_METERS 2.0f #define QUAD_WIDTH_METERS 1.6f // The menu launcher (just a "Keyboard" button - see MenuOverlay.java) is // small and sits above where the keyboard defaults to; the keyboard keeps // its previous size/position. They're independent XrOverlay instances (see // xr_overlay.h) so both can be shown/hidden/dragged separately - deliberately // positioned apart by default so they don't start out overlapping. #define MENU_JAVA_CLASS "de.ladkau.questshock.MenuOverlay" // 768x552 (was 512x256, then 768x512) - grown to fit the header/tabs/Video // Options content added on top of the original single "Keyboard" button, // then again to fit the stats ribbon below the tab bar; taller in world // space too since MENU_WIDTH_METERS (the physical size) stays fixed and // height_m is derived from it by aspect ratio (see xr_overlay.c's // xr_overlay_init()). #define MENU_WIDTH 768 #define MENU_HEIGHT 552 #define MENU_DISTANCE_METERS 1.3f #define MENU_WIDTH_METERS 0.35f #define MENU_DEFAULT_CENTER_X_METERS 0.0f #define MENU_DEFAULT_CENTER_Y_METERS 0.15f #define KEYBOARD_JAVA_CLASS "de.ladkau.questshock.KeyboardOverlay" #define KEYBOARD_WIDTH 1024 #define KEYBOARD_HEIGHT 768 #define KEYBOARD_DISTANCE_METERS 1.5f #define KEYBOARD_WIDTH_METERS 0.8f #define KEYBOARD_DEFAULT_CENTER_X_METERS 0.0f #define KEYBOARD_DEFAULT_CENTER_Y_METERS -0.45f static XrInstance g_instance = XR_NULL_HANDLE; static XrSystemId g_system_id = XR_NULL_SYSTEM_ID; static XrSession g_session = XR_NULL_HANDLE; static XrSpace g_local_space = XR_NULL_HANDLE; static XrSessionState g_session_state = XR_SESSION_STATE_UNKNOWN; // Distinct from g_session_state - true from a successful xrBeginSession // until xrEndSession/loss/exit. Many runtimes only advance the *state* // (e.g. READY -> SYNCHRONIZED -> VISIBLE -> FOCUSED) in response to the // app actually calling xrWaitFrame/xrBeginFrame/xrEndFrame continuously // after xrBeginSession - gating frame submission on already having // reached SYNCHRONIZED+ (as this used to) is a chicken-and-egg deadlock, // since the runtime never progresses past READY without the frame loop // running in the first place. Frame calls just need the session to have // begun, per the standard OpenXR sample pattern - not any particular // later state. static bool g_session_running = false; static XrSwapchainState g_game_swapchain; static XrOverlay *g_menu_overlay = NULL; static XrOverlay *g_keyboard_overlay = NULL; static XrTime g_predicted_display_time = 0; static bool g_frame_should_render = false; static bool g_have_acquired_game_image = false; // XR_META_performance_metrics support (see xr_session_init_performance_metrics() // below) - only present if the runtime advertised it at xrCreateInstance // time (checked via xr_instance_extension_supported()), since unlike the // two always-required KHR extensions above, this one is a device-capability // extra that a build shouldn't hard-depend on. static bool g_performance_metrics_supported = false; static PFN_xrSetPerformanceMetricsStateMETA g_xrSetPerformanceMetricsStateMETA = NULL; static PFN_xrQueryPerformanceMetricsCounterMETA g_xrQueryPerformanceMetricsCounterMETA = NULL; // Resolved once, at startup, by string-matching the runtime's own // human-readable counter path names (see xr_session_init_performance_metrics()) // rather than hardcoding the exact path strings the spec defines - a // substring match is robust to spec/runtime wording we can't verify without // on-device testing. XR_NULL_PATH (0) if no match was found. static XrPath g_cpu_utilization_path = XR_NULL_PATH; static XrPath g_gpu_utilization_path = XR_NULL_PATH; static bool xr_check(XrResult result, const char *what) { if (XR_SUCCEEDED(result)) return true; char resultString[XR_MAX_RESULT_STRING_SIZE] = {0}; if (g_instance != XR_NULL_HANDLE) xrResultToString(g_instance, result, resultString); LOGE("XR: %s failed: %s (%d)", what, resultString[0] ? resultString : "?", (int)result); return false; } static PFN_xrVoidFunction xr_get_proc(const char *name) { PFN_xrVoidFunction fn = NULL; // xrGetInstanceProcAddr itself works with a NULL instance for the // handful of functions (like xrInitializeLoaderKHR) that must be called // before an instance exists. if (!xr_check(xrGetInstanceProcAddr(g_instance, name, &fn), name)) return NULL; return fn; } // Queried before xrCreateInstance (which is why this takes no instance // handle - xrEnumerateInstanceExtensionProperties is one of the few // functions that works without one) to decide whether it's safe to add // `name` to enabledExtensionNames - passing an unsupported extension there // would fail instance creation outright, unlike a missing optional feature // discovered afterward. static bool xr_instance_extension_supported(const char *name) { uint32_t count = 0; if (!xr_check(xrEnumerateInstanceExtensionProperties(NULL, 0, &count, NULL), "xrEnumerateInstanceExtensionProperties")) return false; XrExtensionProperties *props = (XrExtensionProperties *)malloc(sizeof(XrExtensionProperties) * count); for (uint32_t i = 0; i < count; i++) props[i].type = XR_TYPE_EXTENSION_PROPERTIES; bool supported = false; if (xr_check(xrEnumerateInstanceExtensionProperties(NULL, count, &count, props), "xrEnumerateInstanceExtensionProperties")) { for (uint32_t i = 0; i < count; i++) { if (strcmp(props[i].extensionName, name) == 0) { supported = true; break; } } } free(props); return supported; } // Case-insensitively checks whether `needle` occurs anywhere in `haystack` - // used to identify a counter path (e.g. ".../device/cpu_utilization_average") // by a short, stable substring instead of matching its exact full spelling. static bool xr_path_name_contains(const char *haystack, const char *needle) { size_t hlen = strlen(haystack); char lower[256]; if (hlen >= sizeof(lower)) hlen = sizeof(lower) - 1; for (size_t i = 0; i < hlen; i++) lower[i] = (char)tolower((unsigned char)haystack[i]); lower[hlen] = '\0'; return strstr(lower, needle) != NULL; } // Enables XR_META_performance_metrics (if the runtime advertised it - see // xr_instance_extension_supported() above) and resolves the CPU/GPU // device-utilization counter paths xr_session_update_frame_stats() polls // every frame. Called once, right after xrCreateSession succeeds. Leaves // g_cpu_utilization_path/g_gpu_utilization_path at XR_NULL_PATH (their // getters then report -1.0f) if anything here doesn't pan out - this is a // nice-to-have stats display, not something worth failing session bring-up // over. static void xr_session_init_performance_metrics(void) { if (!g_performance_metrics_supported) return; PFN_xrEnumeratePerformanceMetricsCounterPathsMETA enumeratePaths = (PFN_xrEnumeratePerformanceMetricsCounterPathsMETA)xr_get_proc( "xrEnumeratePerformanceMetricsCounterPathsMETA"); g_xrSetPerformanceMetricsStateMETA = (PFN_xrSetPerformanceMetricsStateMETA)xr_get_proc( "xrSetPerformanceMetricsStateMETA"); g_xrQueryPerformanceMetricsCounterMETA = (PFN_xrQueryPerformanceMetricsCounterMETA)xr_get_proc( "xrQueryPerformanceMetricsCounterMETA"); if (enumeratePaths == NULL || g_xrSetPerformanceMetricsStateMETA == NULL || g_xrQueryPerformanceMetricsCounterMETA == NULL) return; XrPerformanceMetricsStateMETA state = {XR_TYPE_PERFORMANCE_METRICS_STATE_META}; state.enabled = XR_TRUE; if (!xr_check(g_xrSetPerformanceMetricsStateMETA(g_session, &state), "xrSetPerformanceMetricsStateMETA")) return; uint32_t pathCount = 0; if (!xr_check(enumeratePaths(g_instance, 0, &pathCount, NULL), "xrEnumeratePerformanceMetricsCounterPathsMETA")) return; XrPath *paths = (XrPath *)malloc(sizeof(XrPath) * pathCount); if (!xr_check(enumeratePaths(g_instance, pathCount, &pathCount, paths), "xrEnumeratePerformanceMetricsCounterPathsMETA")) { free(paths); return; } for (uint32_t i = 0; i < pathCount; i++) { char pathName[256]; uint32_t pathNameLen = 0; if (!xr_check(xrPathToString(g_instance, paths[i], sizeof(pathName), &pathNameLen, pathName), "xrPathToString")) continue; if (g_cpu_utilization_path == XR_NULL_PATH && xr_path_name_contains(pathName, "cpu_utilization")) g_cpu_utilization_path = paths[i]; if (g_gpu_utilization_path == XR_NULL_PATH && xr_path_name_contains(pathName, "gpu_utilization")) g_gpu_utilization_path = paths[i]; } free(paths); LOGI("XR: performance metrics cpu_utilization=%s gpu_utilization=%s", g_cpu_utilization_path != XR_NULL_PATH ? "found" : "not found", g_gpu_utilization_path != XR_NULL_PATH ? "found" : "not found"); } // The Android OpenXR loader needs the JavaVM/context before anything else - // separate from XR_KHR_android_create_instance below, which only affects // xrCreateInstance itself. Skipping this is a common cause of // xrCreateInstance silently failing to find a runtime on Android. static bool xr_initialize_android_loader(JavaVM *vm, jobject activity) { PFN_xrInitializeLoaderKHR initializeLoader = (PFN_xrInitializeLoaderKHR)xr_get_proc("xrInitializeLoaderKHR"); if (initializeLoader == NULL) { LOGE("XR: xrInitializeLoaderKHR not available - no Android OpenXR loader present?"); return false; } XrLoaderInitInfoAndroidKHR loaderInitInfo = {XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR}; loaderInitInfo.applicationVM = vm; loaderInitInfo.applicationContext = activity; return xr_check(initializeLoader((const XrLoaderInitInfoBaseHeaderKHR *)&loaderInitInfo), "xrInitializeLoaderKHR"); } // eglGetCurrentDisplay()/eglGetCurrentContext() hand us the display/context, // but not the EGLConfig they were created with - eglQueryContext's // EGL_CONFIG_ID plus eglChooseConfig is the standard way to recover it (the // same trick Khronos' own hello_xr sample uses). static EGLConfig xr_get_current_egl_config(EGLDisplay display, EGLContext context) { EGLint configId = 0; eglQueryContext(display, context, EGL_CONFIG_ID, &configId); EGLint attribs[] = {EGL_CONFIG_ID, configId, EGL_NONE}; EGLConfig config = NULL; EGLint numConfigs = 0; eglChooseConfig(display, attribs, &config, 1, &numConfigs); return config; } static bool xr_create_instance_and_session(int game_width, int game_height) { JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv(); jobject activity = (jobject)SDL_AndroidGetActivity(); if (env == NULL || activity == NULL) { LOGE("XR: no JNIEnv/Activity from SDL - can't init OpenXR"); return false; } JavaVM *vm = NULL; (*env)->GetJavaVM(env, &vm); if (!xr_initialize_android_loader(vm, activity)) return false; g_performance_metrics_supported = xr_instance_extension_supported(XR_META_PERFORMANCE_METRICS_EXTENSION_NAME); const char *extensions[3] = { XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME, XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME, XR_META_PERFORMANCE_METRICS_EXTENSION_NAME, }; XrInstanceCreateInfoAndroidKHR androidInfo = {XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR}; androidInfo.applicationVM = vm; androidInfo.applicationActivity = activity; XrInstanceCreateInfo createInfo = {XR_TYPE_INSTANCE_CREATE_INFO}; createInfo.next = &androidInfo; createInfo.enabledExtensionCount = g_performance_metrics_supported ? 3 : 2; createInfo.enabledExtensionNames = extensions; strncpy(createInfo.applicationInfo.applicationName, "QuestShock", XR_MAX_APPLICATION_NAME_SIZE - 1); strncpy(createInfo.applicationInfo.engineName, "Shockolate", XR_MAX_ENGINE_NAME_SIZE - 1); createInfo.applicationInfo.applicationVersion = 1; createInfo.applicationInfo.engineVersion = 1; createInfo.applicationInfo.apiVersion = XR_CURRENT_API_VERSION; if (!xr_check(xrCreateInstance(&createInfo, &g_instance), "xrCreateInstance")) return false; XrSystemGetInfo systemInfo = {XR_TYPE_SYSTEM_GET_INFO}; systemInfo.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; if (!xr_check(xrGetSystem(g_instance, &systemInfo, &g_system_id), "xrGetSystem")) return false; // Required by spec before creating an OpenGL ES-backed session, even // though we don't gate on the returned min/max API version here. PFN_xrGetOpenGLESGraphicsRequirementsKHR getGLESRequirements = (PFN_xrGetOpenGLESGraphicsRequirementsKHR)xr_get_proc( "xrGetOpenGLESGraphicsRequirementsKHR"); if (getGLESRequirements == NULL) return false; XrGraphicsRequirementsOpenGLESKHR glesRequirements = { XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR}; if (!xr_check(getGLESRequirements(g_instance, g_system_id, &glesRequirements), "xrGetOpenGLESGraphicsRequirementsKHR")) return false; EGLDisplay display = eglGetCurrentDisplay(); EGLContext context = eglGetCurrentContext(); if (display == EGL_NO_DISPLAY || context == EGL_NO_CONTEXT) { LOGE("XR: no current EGL display/context - call xr_init() after init_opengl()"); return false; } XrGraphicsBindingOpenGLESAndroidKHR graphicsBinding = { XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR}; graphicsBinding.display = display; graphicsBinding.config = xr_get_current_egl_config(display, context); graphicsBinding.context = context; XrSessionCreateInfo sessionInfo = {XR_TYPE_SESSION_CREATE_INFO}; sessionInfo.next = &graphicsBinding; sessionInfo.systemId = g_system_id; if (!xr_check(xrCreateSession(g_instance, &sessionInfo, &g_session), "xrCreateSession")) return false; xr_session_init_performance_metrics(); XrReferenceSpaceCreateInfo spaceInfo = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; spaceInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL; spaceInfo.poseInReferenceSpace.orientation.w = 1.0f; if (!xr_check(xrCreateReferenceSpace(g_session, &spaceInfo, &g_local_space), "xrCreateReferenceSpace")) return false; // 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, 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); xrEnumerateSwapchainFormats(g_session, formatCount, &formatCount, formats); int64_t chosenFormat = formats[0]; for (uint32_t i = 0; i < formatCount; i++) { LOGI("XR: swapchain format[%u] = 0x%llx", i, (unsigned long long)formats[i]); if (formats[i] == GL_RGBA8) { chosenFormat = GL_RGBA8; break; } } 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"); return false; } // Both non-fatal - the game keeps rendering with no such panel if // either fails (e.g. the Java class couldn't be resolved via JNI). XrOverlayConfig menuConfig = { .java_class_name = MENU_JAVA_CLASS, .width = MENU_WIDTH, .height = MENU_HEIGHT, .distance_m = MENU_DISTANCE_METERS, .width_m = MENU_WIDTH_METERS, .default_center_x_m = MENU_DEFAULT_CENTER_X_METERS, .default_center_y_m = MENU_DEFAULT_CENTER_Y_METERS, }; g_menu_overlay = xr_overlay_create(g_instance, g_session, chosenFormat, &menuConfig); XrOverlayConfig keyboardConfig = { .java_class_name = KEYBOARD_JAVA_CLASS, .width = KEYBOARD_WIDTH, .height = KEYBOARD_HEIGHT, .distance_m = KEYBOARD_DISTANCE_METERS, .width_m = KEYBOARD_WIDTH_METERS, .default_center_x_m = KEYBOARD_DEFAULT_CENTER_X_METERS, .default_center_y_m = KEYBOARD_DEFAULT_CENTER_Y_METERS, }; g_keyboard_overlay = xr_overlay_create(g_instance, g_session, chosenFormat, &keyboardConfig); LOGI("XR: instance/session ready (game %dx%d, menu %dx%d, keyboard %dx%d)", game_width, game_height, MENU_WIDTH, MENU_HEIGHT, KEYBOARD_WIDTH, KEYBOARD_HEIGHT); return true; } bool xr_init(int game_width, int game_height) { if (!xr_create_instance_and_session(game_width, game_height)) { LOGE("XR: bring-up failed - falling back to the normal window present path"); xr_shutdown(); return false; } return true; } XrOverlay *xr_session_get_menu_overlay(void) { return g_menu_overlay; } XrOverlay *xr_session_get_keyboard_overlay(void) { return g_keyboard_overlay; } void xr_poll_events(void) { if (g_instance == XR_NULL_HANDLE) return; while (true) { XrEventDataBuffer event = {XR_TYPE_EVENT_DATA_BUFFER}; XrResult result = xrPollEvent(g_instance, &event); if (result == XR_EVENT_UNAVAILABLE) break; if (!xr_check(result, "xrPollEvent")) break; if (event.type == XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) { XrEventDataSessionStateChanged *stateEvent = (XrEventDataSessionStateChanged *)&event; g_session_state = stateEvent->state; LOGI("XR: session state -> %d", (int)g_session_state); if (g_session_state == XR_SESSION_STATE_READY) { XrSessionBeginInfo beginInfo = {XR_TYPE_SESSION_BEGIN_INFO}; beginInfo.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; g_session_running = xr_check(xrBeginSession(g_session, &beginInfo), "xrBeginSession"); } else if (g_session_state == XR_SESSION_STATE_STOPPING) { xr_check(xrEndSession(g_session), "xrEndSession"); g_session_running = false; } else if (g_session_state == XR_SESSION_STATE_EXITING || g_session_state == XR_SESSION_STATE_LOSS_PENDING) { g_session_running = false; } } } // xrPollEvent() leaves gl4es reporting a stray GL_INVALID_OPERATION on // every call while the session is running - drain it (looped, since // more than one can accumulate across xrPollEvent()'s possibly-multiple // iterations above) so it doesn't surface as a false positive at the // next unrelated glGetError() check in this app's own rendering code. while (glGetError() != GL_NO_ERROR) { } } bool xr_is_session_running(void) { return g_session_running; } bool xr_frame_begin(void) { g_have_acquired_game_image = false; if (!xr_is_session_running()) return false; XrFrameWaitInfo waitInfo = {XR_TYPE_FRAME_WAIT_INFO}; XrFrameState frameState = {XR_TYPE_FRAME_STATE}; if (!xr_check(xrWaitFrame(g_session, &waitInfo, &frameState), "xrWaitFrame")) return false; g_predicted_display_time = frameState.predictedDisplayTime; g_frame_should_render = frameState.shouldRender; XrFrameBeginInfo beginInfo = {XR_TYPE_FRAME_BEGIN_INFO}; if (!xr_check(xrBeginFrame(g_session, &beginInfo), "xrBeginFrame")) return false; if (!g_frame_should_render) return false; g_have_acquired_game_image = xr_swapchain_acquire(g_instance, &g_game_swapchain); return g_have_acquired_game_image; } // Backs xr_session_get_fps()/get_frame_ms() (see xr_session.h) - exponential // smoothing (rather than a plain per-frame instantaneous value) so // MenuOverlay's header display doesn't visibly jitter every single frame. // Called once per xr_frame_end() that reaches its own end (i.e. once per // real XR frame, whether or not the game itself actually rendered anything // new into it this frame - matches the OpenXR compositor's own frame // cadence, which is what "FPS" means to someone looking at this display). static float s_fps = 0.0f; static float s_frame_ms = 0.0f; static float s_cpu_utilization = -1.0f; static float s_gpu_utilization = -1.0f; // Reads one XR_META_performance_metrics counter (if it was found at // startup - see xr_session_init_performance_metrics()) and folds it into // *smoothed with the same exponential smoothing as FPS/frame time above. // Leaves *smoothed at -1.0f (its "unsupported" sentinel) untouched if the // path wasn't found, the query fails, or the runtime didn't mark its // floatValue valid this call. static void xr_session_update_utilization(XrPath path, float *smoothed) { if (path == XR_NULL_PATH || g_xrQueryPerformanceMetricsCounterMETA == NULL) return; XrPerformanceMetricsCounterMETA counter = {XR_TYPE_PERFORMANCE_METRICS_COUNTER_META}; if (!xr_check(g_xrQueryPerformanceMetricsCounterMETA(g_session, path, &counter), "xrQueryPerformanceMetricsCounterMETA")) return; if (!(counter.counterFlags & XR_PERFORMANCE_METRICS_COUNTER_FLOAT_VALUE_VALID_BIT_META)) return; const float kSmoothing = 0.1f; *smoothed = (*smoothed < 0.0f) ? counter.floatValue : *smoothed * (1.0f - kSmoothing) + counter.floatValue * kSmoothing; } static void xr_session_update_frame_stats(void) { static Uint32 s_last_ticks = 0; Uint32 now = SDL_GetTicks(); if (s_last_ticks != 0) { Uint32 delta_ms = now - s_last_ticks; if (delta_ms > 0) { float instant_fps = 1000.0f / (float)delta_ms; const float kSmoothing = 0.1f; s_fps = (s_fps == 0.0f) ? instant_fps : s_fps * (1.0f - kSmoothing) + instant_fps * kSmoothing; s_frame_ms = (s_frame_ms == 0.0f) ? (float)delta_ms : s_frame_ms * (1.0f - kSmoothing) + (float)delta_ms * kSmoothing; } } s_last_ticks = now; xr_session_update_utilization(g_cpu_utilization_path, &s_cpu_utilization); xr_session_update_utilization(g_gpu_utilization_path, &s_gpu_utilization); } float xr_session_get_fps(void) { return s_fps; } float xr_session_get_frame_ms(void) { return s_frame_ms; } float xr_session_get_cpu_utilization(void) { return s_cpu_utilization; } float xr_session_get_gpu_utilization(void) { return s_gpu_utilization; } void xr_frame_end(void) { if (g_session == XR_NULL_HANDLE) return; // Syncs actions and hit-tests/dispatches each hand's ray against // keyboard/menu/game quad (see xr_input.c) before the game swapchain // image is released below - this always runs, even when there's // nothing to draw (no acquired image this frame), so edge detection // (trigger/menu-button clicks) doesn't miss a frame. Any resulting // laser-beam quads are submitted as their own composition layers // further down, not drawn into the game swapchain itself. if (xr_is_session_running()) xr_input_sync_and_draw(g_local_space, g_predicted_display_time, g_have_acquired_game_image); if (g_have_acquired_game_image) xr_swapchain_release(g_instance, &g_game_swapchain); if (!xr_is_session_running()) return; // 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 - and, per hand, a // laser-beam quad while that hand's ray is aimed at the keyboard, // menu, or the game quad itself (see xr_input_get_beam_layer()). XrCompositionLayerQuad gameQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD}; gameQuad.space = g_local_space; gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH; gameQuad.subImage.swapchain = g_game_swapchain.swapchain; gameQuad.subImage.imageRect.extent.width = g_game_swapchain.width; gameQuad.subImage.imageRect.extent.height = g_game_swapchain.height; gameQuad.pose.orientation.w = 1.0f; gameQuad.pose.position.z = -QUAD_DISTANCE_METERS; gameQuad.size.width = QUAD_WIDTH_METERS; gameQuad.size.height = QUAD_WIDTH_METERS * (float)g_game_swapchain.height / (float)g_game_swapchain.width; const XrCompositionLayerBaseHeader *layers[5]; uint32_t layerCount = 0; if (g_have_acquired_game_image) layers[layerCount++] = (XrCompositionLayerBaseHeader *)&gameQuad; XrCompositionLayerQuad menuQuad; if (g_frame_should_render && xr_overlay_render_and_build_layer(g_menu_overlay, &menuQuad)) { menuQuad.space = g_local_space; menuQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH; layers[layerCount++] = (XrCompositionLayerBaseHeader *)&menuQuad; } XrCompositionLayerQuad keyboardQuad; if (g_frame_should_render && xr_overlay_render_and_build_layer(g_keyboard_overlay, &keyboardQuad)) { keyboardQuad.space = g_local_space; keyboardQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH; // Lets the keyboard sit on screen translucently while playing // rather than fully hiding whatever's behind it - KeyboardOverlay // writes real sub-255 alpha into the swapchain texture (see // OverlayPanel.compositeAndPublish()), already in Android's // default premultiplied format, which is what this flag's absence // of UNPREMULTIPLIED_ALPHA_BIT assumes. The menu launcher above // deliberately doesn't get this flag - it stays fully opaque. keyboardQuad.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; 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; endInfo.layerCount = layerCount; endInfo.layers = layerCount > 0 ? layers : NULL; xr_check(xrEndFrame(g_session, &endInfo), "xrEndFrame"); xr_session_update_frame_stats(); } void xr_shutdown(void) { xr_swapchain_destroy(&g_game_swapchain); if (g_local_space != XR_NULL_HANDLE) xrDestroySpace(g_local_space); g_local_space = XR_NULL_HANDLE; // Before the session/instance they were created from. xr_input_shutdown(); xr_overlay_destroy(g_menu_overlay); g_menu_overlay = NULL; xr_overlay_destroy(g_keyboard_overlay); g_keyboard_overlay = NULL; if (g_session != XR_NULL_HANDLE) xrDestroySession(g_session); g_session = XR_NULL_HANDLE; if (g_instance != XR_NULL_HANDLE) xrDestroyInstance(g_instance); g_instance = XR_NULL_HANDLE; g_session_state = XR_SESSION_STATE_UNKNOWN; g_session_running = false; g_performance_metrics_supported = false; g_xrSetPerformanceMetricsStateMETA = NULL; g_xrQueryPerformanceMetricsCounterMETA = NULL; g_cpu_utilization_path = XR_NULL_PATH; g_gpu_utilization_path = XR_NULL_PATH; } void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m) { *distance_m = QUAD_DISTANCE_METERS; *half_width_m = QUAD_WIDTH_METERS * 0.5f; *half_height_m = QUAD_WIDTH_METERS * 0.5f * (float)g_game_swapchain.height / (float)g_game_swapchain.width; } void xr_get_game_resolution(int *width, int *height) { // Not g_game_swapchain.width/height: that's the fixed physical size of // the swapchain image the game is composited into (see xr_init()) - // android_draw_surface_as_quad() (OpenGL.cc, engine-patches/11) always // stretch-blits drawSurface to fill that image regardless of // drawSurface's own pixel size, so the swapchain's size has no relation // to the coordinate space MouseX/MouseY actually live in. gScreenWide/ // gScreenHigh (InitMac.h) is drawSurface's current size - the // resolution the in-game options menu can change at runtime // (ShockBitmap.c's ChangeScreenSize()) independently of the swapchain, // which is created once at launch and never resized. Using the fixed // swapchain size here after such a change mapped the laser pointer's // quad-space hit into the wrong coordinate space entirely, moving // MouseX/MouseY out of the engine's actual screen bounds - which is // what made the cursor disappear and become unclickable. extern int32_t gScreenWide, gScreenHigh; *width = gScreenWide; *height = gScreenHigh; }