Frame: size the eye image from vr_resolution_scale, ask for a refresh rate

The Frame build rendered the runtime's 1728 square recommendation, but the
panel is 2160 square. The eye image is now the recommended size times
vr_resolution_scale (archived, 1.25 by default, which is 2160), held to what
the view (maxImageRect) and the swapchain allow; the engine's screen size is
set to match before the swapchains are made. The log has the recommended size,
the largest the view and swapchain take, and what was used. The HUD shift
and the rest work from the real width, so they follow.

vr_refresh_rate (archived, default 90, standard section 9) is asked for
with xrRequestDisplayRefreshRateFB: the highest the runtime offers that isn't
over it. The offered list, the request and the result are logged. The
earlier "now: 90 Hz" at a 13.89 ms frame period was a mismatch in what
was read, not asked for; vr_refresh follows the real frame period.

Frame build only. Builds, haven't run it on the headset yet.
This commit is contained in:
Madison committed 2026-10-05 02:02:49 +09:00
1 parent a4f12f84fd
commit afbc400cfe
5 files changed
+121 -31

No files matched your search

@@ -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())
@@ -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),
@@ -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
@@ -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
+7 -2
View File
@@ -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.