Repeat the last frame on every refresh the game has no new frame for

SteamVR on the Steam Frame ran the game at half the 120 Hz display rate,
since render-first pacing submits only when a game frame is sealed, and
filled every other refresh itself even with Motion Smoothing off, which
doubled the HUD and the menu screen while the head turned. With the new
[vr] repeat_frames (default on for the Frame, off elsewhere, live), the
pacing thread waits a millisecond for the eyes and otherwise spends the
refresh on a keep-alive cycle that resubmits the retained layer with its
rendered poses, paced by xrWaitFrame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
This commit is contained in:
Claude committed 2026-10-04 16:16:37 +00:00
1 parent 60fdb4b447
commit 8718d1ddd2
6 files changed
+77 -5

No files matched your search

+27
View File
@@ -100,6 +100,7 @@ struct RuntimeUserConfig {
std::optional<std::string> vrPerformanceLevel;
std::optional<std::string> vrFoveation;
std::optional<bool> vrEyeTrackedFoveation;
std::optional<bool> vrRepeatFrames;
std::optional<std::string> vrRecenterKey;
std::optional<float> vrLeanBackDegrees;
// F10 > Diagnostics: OpenXR pacing and presentation logging in console.log.
@@ -338,6 +339,19 @@ inline constexpr bool kVrEyeTrackedFoveationDefault = true;
inline constexpr bool kVrEyeTrackedFoveationDefault = false;
#endif
// Repeated frames: while the game has no new frame for a display refresh, the last one is submitted
// again with the head pose it was rendered for, which the compositor turns to the current one. A
// runtime then sees the app at the display's rate instead of at the game's 60 FPS, and does not
// halve the app's rate and fill every other refresh itself: SteamVR's filled frames double the HUD
// and the menu screen while the head turns. On by default on the Steam Frame. Live.
#if defined(MKW_HEADSET_STEAM_FRAME)
inline constexpr bool kVrRepeatFramesDefault = true;
#define MKW_VR_REPEAT_FRAMES_DEFAULT_TOML "true"
#else
inline constexpr bool kVrRepeatFramesDefault = false;
#define MKW_VR_REPEAT_FRAMES_DEFAULT_TOML "false"
#endif
// The level aurora_set_stereo_foveation takes; anything unknown is off.
inline uint32_t VrFoveationLevelIndex(std::string_view value) {
const auto it = std::find(kVrFoveationLevels.begin(), kVrFoveationLevels.end(), value);
@@ -590,6 +604,9 @@ inline void EnsureConfigFile() {
"# at 60, so 120 shows every frame twice. Only runtimes that let apps\n"
"# choose (XR_FB_display_refresh_rate) take it. Live.\n"
"refresh_rate = " MKW_VR_REFRESH_RATE_DEFAULT_TEXT "\n"
"# Submit the last frame again for each refresh the game has no new\n"
"# frame for, so the runtime does not fill those refreshes itself. Live.\n"
"repeat_frames = " MKW_VR_REPEAT_FRAMES_DEFAULT_TOML "\n"
"render_scale = " MKW_VR_RENDER_SCALE_DEFAULT_TEXT "\n"
"world_units_per_meter = 500.0\n"
"hud_distance_meters = 2.0\n"
@@ -910,6 +927,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.vrFoveation = *value;
}
config.vrEyeTrackedFoveation = FindConfigValue<bool>(document, "vr", "eye_tracked_foveation");
config.vrRepeatFrames = FindConfigValue<bool>(document, "vr", "repeat_frames");
if (auto value = FindConfigValue<std::string>(document, "vr", "mirror_view");
value && IsSupportedVrMirrorView(*value)) {
config.vrMirrorView = *value;
@@ -1322,6 +1340,11 @@ inline bool SetVrFoveation(std::string value) {
return WriteSetting("vr", "foveation", FormatString(value));
}
inline bool SetVrRepeatFrames(bool value) {
Mutable().vrRepeatFrames = value;
return WriteSetting("vr", "repeat_frames", value ? "true" : "false");
}
inline bool SetVrEyeTrackedFoveation(bool value) {
Mutable().vrEyeTrackedFoveation = value;
return WriteSetting("vr", "eye_tracked_foveation", value ? "true" : "false");
@@ -1853,6 +1876,10 @@ inline std::string VrFoveation(std::string fallback = kVrFoveationDefault) {
return value && IsSupportedVrFoveation(*value) ? *value : std::move(fallback);
}
inline bool VrRepeatFrames(bool fallback = kVrRepeatFramesDefault) {
return Get().vrRepeatFrames.value_or(fallback);
}
inline bool VrEyeTrackedFoveation(bool fallback = kVrEyeTrackedFoveationDefault) {
return Get().vrEyeTrackedFoveation.value_or(fallback);
}