diff --git a/Projects/Android/jni/src/Lambda1VR/L1VR_SurfaceView.c b/Projects/Android/jni/src/Lambda1VR/L1VR_SurfaceView.c index 388cd03..53d0c3b 100644 --- a/Projects/Android/jni/src/Lambda1VR/L1VR_SurfaceView.c +++ b/Projects/Android/jni/src/Lambda1VR/L1VR_SurfaceView.c @@ -500,6 +500,8 @@ convar_t *vr_use_gesture_boundary; #ifdef L1VR_STEAM_FRAME convar_t *vr_hud_distance; convar_t *vr_hud_level; +convar_t *vr_refresh_rate; +convar_t *vr_resolution_scale; #endif @@ -580,6 +582,8 @@ void VR_Init() } } vr_hud_distance = Cvar_Get( "vr_hud_distance", "1.0", CVAR_ARCHIVE, "How far ahead the HUD sits, in metres" ); + vr_refresh_rate = Cvar_Get( "vr_refresh_rate", "90", CVAR_ARCHIVE, "The display refresh rate to ask for, the highest the runtime offers that isn't over this" ); + vr_resolution_scale = Cvar_Get( "vr_resolution_scale", "1.25", CVAR_ARCHIVE, "Eye image size as a multiple of the runtime's recommended one (1.25 is the panel's 2160 on the Steam Frame), 0.5 to 2" ); vr_hud_level = Cvar_Get( "vr_hud_level", "1", CVAR_ARCHIVE, "1 keeps the HUD, text, and camera-facing sprites level when the head rolls, 0 lets them roll with the head" ); #endif @@ -664,6 +668,14 @@ void * AppThreadFunction( void * parm ) VR_Init(); TBXR_EnterVR(); +#ifdef L1VR_STEAM_FRAME + { + // the eye image size, from vr_resolution_scale (the engine set up its screen before it was known) + extern void R_ChangeDisplaySettingsFast( int width, int height ); + TBXR_SetEyeScale( vr_resolution_scale->value ); + R_ChangeDisplaySettingsFast( (int)gAppState.Width, (int)gAppState.Height ); + } +#endif TBXR_InitRenderer(); TBXR_InitActions(); TBXR_WaitForSessionActive(); @@ -687,15 +699,16 @@ void * AppThreadFunction( void * parm ) { //The positional movement scaling goes by vr_refresh. SteamVR picks the rate for //the app, so take it from the frame period rather than from what's on offer. - static bool refreshSet = false; + static int lastRate = 0; const XrDuration period = gAppState.FrameState.predictedDisplayPeriod; - if (!refreshSet && period > 0) + const int rate = period > 0 ? (int)(1e9 / (double)period + 0.5) : 0; + if (rate > 0 && rate != lastRate) { - char rate[16]; - Q_snprintf(rate, sizeof(rate), "%d", (int)(1e9 / (double)period + 0.5)); - Cvar_Set2("vr_refresh", rate, true); - ALOGI("[openxr] frame period %.2f ms, vr_refresh %s", period / 1e6, rate); - refreshSet = true; + char text[16]; + Q_snprintf(text, sizeof(text), "%d", rate); + Cvar_Set2("vr_refresh", text, true); + ALOGI("[openxr] frame period %.2f ms, vr_refresh %s", period / 1e6, text); + lastRate = rate; } } if (!TBXR_ShouldRender()) diff --git a/Projects/Android/jni/src/Lambda1VR/TBXR_Common.c b/Projects/Android/jni/src/Lambda1VR/TBXR_Common.c index 446e9ca..7022b3d 100644 --- a/Projects/Android/jni/src/Lambda1VR/TBXR_Common.c +++ b/Projects/Android/jni/src/Lambda1VR/TBXR_Common.c @@ -1486,6 +1486,91 @@ void ovrTrackedController_Clear(ovrTrackedController* controller) { controller->Pose = XrPosef_Identity(); } +#ifdef L1VR_STEAM_FRAME +static uint32_t tbxrMaxSwapchain[2]; + +/* +The eye image size: what the runtime recommends times a scale, held to what the view and the +swapchain can take. The scale is vr_resolution_scale (standard.md section 9). The panel is 2160x2160 +a eye and the runtime recommends 1728, so 1.25 is native. Returns the scale that was used. +*/ +float TBXR_SetEyeScale(float scale) +{ + const uint32_t recW = gAppState.ViewConfigurationView[0].recommendedImageRectWidth; + const uint32_t recH = gAppState.ViewConfigurationView[0].recommendedImageRectHeight; + uint32_t maxW = gAppState.ViewConfigurationView[0].maxImageRectWidth; + uint32_t maxH = gAppState.ViewConfigurationView[0].maxImageRectHeight; + if (tbxrMaxSwapchain[0] && (maxW == 0 || tbxrMaxSwapchain[0] < maxW)) maxW = tbxrMaxSwapchain[0]; + if (tbxrMaxSwapchain[1] && (maxH == 0 || tbxrMaxSwapchain[1] < maxH)) maxH = tbxrMaxSwapchain[1]; + + if (scale < 0.5f) scale = 0.5f; + if (scale > 2.0f) scale = 2.0f; + scale *= SS_MULTIPLIER; // 1.0 unless -s was given + + uint32_t w = (uint32_t)(recW * scale + 0.5f); + uint32_t h = (uint32_t)(recH * scale + 0.5f); + if (maxW && w > maxW) w = maxW; + if (maxH && h > maxH) h = maxH; + w &= ~1u; // even + h &= ~1u; + + gAppState.Width = (float)w; + gAppState.Height = (float)h; + ALOGI("[openxr] eye image: recommended %ux%u, largest the view takes %ux%u (swapchain %ux%u), scale %.2f, using %ux%u", + recW, recH, + gAppState.ViewConfigurationView[0].maxImageRectWidth, gAppState.ViewConfigurationView[0].maxImageRectHeight, + tbxrMaxSwapchain[0], tbxrMaxSwapchain[1], scale, w, h); + return scale; +} + +/* +Ask for the highest refresh rate the runtime offers that isn't over the setting (vr_refresh_rate). SteamVR +offers an app only the rate set for it in its own per-app settings, so this is often the only one there is. +*/ +static void TBXR_ChooseRefreshRate(float wanted) +{ + PFN_xrEnumerateDisplayRefreshRatesFB pfnEnumerateRates = NULL; + PFN_xrGetDisplayRefreshRateFB pfnGetRate = NULL; + PFN_xrRequestDisplayRefreshRateFB pfnRequestRate = NULL; + xrGetInstanceProcAddr(gAppState.Instance, "xrEnumerateDisplayRefreshRatesFB", (PFN_xrVoidFunction*)(&pfnEnumerateRates)); + xrGetInstanceProcAddr(gAppState.Instance, "xrGetDisplayRefreshRateFB", (PFN_xrVoidFunction*)(&pfnGetRate)); + xrGetInstanceProcAddr(gAppState.Instance, "xrRequestDisplayRefreshRateFB", (PFN_xrVoidFunction*)(&pfnRequestRate)); + if (pfnEnumerateRates == NULL || pfnGetRate == NULL || pfnRequestRate == NULL) + return; + + float rates[16]; + uint32_t numRates = 0; + pfnEnumerateRates(gAppState.Session, 0, &numRates, NULL); + if (numRates > 16) + numRates = 16; + pfnEnumerateRates(gAppState.Session, numRates, &numRates, rates); + + float best = 0.0f, lowest = 0.0f; + char list[160] = ""; + for (uint32_t i = 0; i < numRates; i++) + { + char one[24]; + snprintf(one, sizeof(one), "%s%.0f", i ? ", " : "", rates[i]); + strncat(list, one, sizeof(list) - strlen(list) - 1); + if (rates[i] <= wanted + 0.5f && rates[i] > best) best = rates[i]; + if (lowest == 0.0f || rates[i] < lowest) lowest = rates[i]; + } + if (best == 0.0f) + best = lowest; + + float before = 0.0f; + pfnGetRate(gAppState.Session, &before); + XrResult result = XR_SUCCESS; + if (best > 0.0f && fabsf(best - before) > 0.5f) + result = pfnRequestRate(gAppState.Session, best); + float after = 0.0f; + pfnGetRate(gAppState.Session, &after); + ALOGI("[openxr] refresh rate: offered %s Hz, wanted %.0f, asked for %.0f (now %.1f, was %.1f, result %d)", + list, wanted, best, after, before, (int)result); + gAppState.currentDisplayRefreshRate = after; +} +#endif + void TBXR_InitialiseResolution() { // Enumerate the viewport configurations. @@ -1723,30 +1808,10 @@ void TBXR_InitRenderer( ) { } #ifdef L1VR_STEAM_FRAME - //Just look at the refresh rate, SteamVR only offers the one set for the app + //Pick a refresh rate, SteamVR only offers the one set for the app if (TBXR_ExtensionEnabled(XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME)) { - PFN_xrEnumerateDisplayRefreshRatesFB pfnEnumerateRates = NULL; - PFN_xrGetDisplayRefreshRateFB pfnGetRate = NULL; - xrGetInstanceProcAddr(gAppState.Instance, "xrEnumerateDisplayRefreshRatesFB", (PFN_xrVoidFunction*)(&pfnEnumerateRates)); - xrGetInstanceProcAddr(gAppState.Instance, "xrGetDisplayRefreshRateFB", (PFN_xrVoidFunction*)(&pfnGetRate)); - if (pfnEnumerateRates != NULL && pfnGetRate != NULL) - { - float rates[16]; - uint32_t numRates = 0; - pfnEnumerateRates(gAppState.Session, 0, &numRates, NULL); - if (numRates > 16) - { - numRates = 16; - } - pfnEnumerateRates(gAppState.Session, numRates, &numRates, rates); - for (uint32_t i = 0; i < numRates; i++) - { - ALOGI("[openxr] display refresh rate offered: %.1f Hz", rates[i]); - } - pfnGetRate(gAppState.Session, &gAppState.currentDisplayRefreshRate); - ALOGI("[openxr] display refresh rate now: %.1f Hz", gAppState.currentDisplayRefreshRate); - } + TBXR_ChooseRefreshRate(vr_refresh_rate != NULL ? vr_refresh_rate->value : 90.0f); } #endif @@ -1934,6 +1999,8 @@ void TBXR_InitialiseOpenXR() XrSystemProperties systemProperties = {}; systemProperties.type = XR_TYPE_SYSTEM_PROPERTIES; OXR(xrGetSystemProperties(gAppState.Instance, gAppState.SystemId, &systemProperties)); + tbxrMaxSwapchain[0] = systemProperties.graphicsProperties.maxSwapchainImageWidth; + tbxrMaxSwapchain[1] = systemProperties.graphicsProperties.maxSwapchainImageHeight; ALOGI("[openxr] system: %s, OpenGL ES %d.%d to %d.%d wanted by the runtime", systemProperties.systemName, XR_VERSION_MAJOR(graphicsRequirements.minApiVersionSupported), diff --git a/Projects/Android/jni/src/Lambda1VR/TBXR_Common.h b/Projects/Android/jni/src/Lambda1VR/TBXR_Common.h index 6cf9f19..8598840 100644 --- a/Projects/Android/jni/src/Lambda1VR/TBXR_Common.h +++ b/Projects/Android/jni/src/Lambda1VR/TBXR_Common.h @@ -372,6 +372,9 @@ void TBXR_RecenterToHead(bool resetHeight); // The menu laser: where the weapon hand's aim ray meets the flat screen, 0..1 across and down. False // when the screen isn't showing or the hand isn't tracked (the ray can also miss the panel's edge). bool TBXR_ScreenPointer(float* u, float* v); + +// The eye image size from the recommended one times a scale, held to the limits. Before the swapchains are made. +float TBXR_SetEyeScale(float scale); #endif #endif //vrcommon_h \ No newline at end of file diff --git a/Projects/Android/jni/src/Lambda1VR/VrCvars.h b/Projects/Android/jni/src/Lambda1VR/VrCvars.h index 7f0c91b..53ed749 100644 --- a/Projects/Android/jni/src/Lambda1VR/VrCvars.h +++ b/Projects/Android/jni/src/Lambda1VR/VrCvars.h @@ -38,4 +38,6 @@ extern convar_t *vr_stereo_side; #ifdef L1VR_STEAM_FRAME extern convar_t *vr_hud_distance; extern convar_t *vr_hud_level; +extern convar_t *vr_refresh_rate; +extern convar_t *vr_resolution_scale; #endif diff --git a/docs/STEAM_FRAME_VR.md b/docs/STEAM_FRAME_VR.md index 268e67b..1687ae2 100644 --- a/docs/STEAM_FRAME_VR.md +++ b/docs/STEAM_FRAME_VR.md @@ -140,8 +140,13 @@ says the Frame profile is active. Java loads it as `openxr_loader`. - Only OpenGL ES is required from the runtime. Everything else is enabled if the runtime lists it. There's no Pico or Meta code on that build. -- It renders at the runtime's recommended eye size. The Quest build uses 1.3x of - it, which is a lot on the Frame. +- The eye image is the runtime's recommended size (1728 square) times `vr_resolution_scale` (archived, default 1.25, + which is the panel's own 2160 square), held to what the view and the swapchain allow. The Quest build uses 1.3x of the + recommended size. The log has the recommended, the largest allowed and the size used. +- `vr_refresh_rate` (archived, default 90) is the rate to ask for: the highest the runtime offers that isn't + over it, with the offered list, the request and the result in the log. SteamVR only offers an app the rate in + its per-app settings in the headset, so that's often the only one on the list. `vr_refresh`, which the game + uses to scale walking, follows the real frame period. - When the headset is in standby (`shouldRender` is false) the frame goes in with no layers and the game doesn't simulate. Without that the server kept running. - If SteamVR quits the app the game shuts down the normal way.