Overhaul the VR menu UI: header, tabs, live video options, hover/click feedback, i18n, and dragging
build / build (push) Successful in 2m25s
build / build (push) Successful in 2m25s
- Header with title, git-tag-derived version, and a live FPS/frame-time/ CPU/GPU-utilization/free-memory stats ribbon below the tab bar (FPS via a new per-frame tracker in xr_session.c; CPU/GPU via the OpenXR XR_META_performance_metrics extension, gracefully unavailable on runtimes that don't support it) - Bottom tab bar with a live-editable Video Options tab (renderer/ resolution), applying changes immediately via qs_config.c - Hover and press feedback on every menu/keyboard button via a StateListDrawable (Android's default ripple never animates on this permanently unattached view tree), with a lighter default button color for visibility - i18n for all menu/keyboard chrome text, following the game's own language setting (Strings.java) - The main menu is now draggable via its header, matching the keyboard; xr_input.c's drag handling is generalized to per-overlay state instead of being keyboard-only - The keyboard's title bar now matches the menu header's style
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#include "xr_session.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -39,8 +40,14 @@
|
||||
// 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"
|
||||
#define MENU_WIDTH 512
|
||||
#define MENU_HEIGHT 256
|
||||
// 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
|
||||
@@ -79,6 +86,22 @@ 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;
|
||||
@@ -99,6 +122,106 @@ static PFN_xrVoidFunction xr_get_proc(const char *name) {
|
||||
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
|
||||
@@ -146,9 +269,13 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
if (!xr_initialize_android_loader(vm, activity))
|
||||
return false;
|
||||
|
||||
const char *extensions[] = {
|
||||
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};
|
||||
@@ -157,7 +284,7 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
|
||||
XrInstanceCreateInfo createInfo = {XR_TYPE_INSTANCE_CREATE_INFO};
|
||||
createInfo.next = &androidInfo;
|
||||
createInfo.enabledExtensionCount = sizeof(extensions) / sizeof(extensions[0]);
|
||||
createInfo.enabledExtensionCount = g_performance_metrics_supported ? 3 : 2;
|
||||
createInfo.enabledExtensionNames = extensions;
|
||||
strncpy(createInfo.applicationInfo.applicationName, "QuestShock",
|
||||
XR_MAX_APPLICATION_NAME_SIZE - 1);
|
||||
@@ -206,6 +333,8 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
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;
|
||||
@@ -354,6 +483,65 @@ bool xr_frame_begin(void) {
|
||||
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;
|
||||
@@ -444,6 +632,8 @@ void xr_frame_end(void) {
|
||||
endInfo.layers = layerCount > 0 ? layers : NULL;
|
||||
|
||||
xr_check(xrEndFrame(g_session, &endInfo), "xrEndFrame");
|
||||
|
||||
xr_session_update_frame_stats();
|
||||
}
|
||||
|
||||
void xr_shutdown(void) {
|
||||
@@ -470,6 +660,12 @@ void xr_shutdown(void) {
|
||||
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user