Skip to content

Commit b6b5746

Browse files
committed
VR: Handle games whose frame number freezes or restarts
UEVR keys its pose pipeline on the frame number from the game's FSceneViewFamily: poses live in pipeline_states[frame_count % QUEUE_SIZE], the game and render threads are matched by it, and it reaches enqueue_render_poses. OpenXR::on_pre_render_game_thread refreshes a slot only when it is empty or when the key jumped forward past it. Any other revisit keeps the predicted display time already in the slot, so xrLocateViews predicts for a moment that has passed and the image comes loose from the head and swims against it. The game's number revisits slots out of order in at least two measured ways: it freezes while a fullscreen menu is up though the game keeps rendering, held for 159 view families in The Outer Worlds 2, and it restarts from scratch on a level load, in Gylt and Silent Hill 2. Carry the number onto a key that only moves forward: count on while it is frozen, and step one past the highest reached when it restarts. An offset rather than a counter of our own, so a game that does neither keeps its own numbering with the offset at zero. Both publishers of a frame number add it; applying it in one leaves the two disagreeing by the width of a stall, and a pose stored under one key is looked up under another. Progress is "exceeded the highest number seen", not "differs from the previous one": a game can push several view families per frame, so the previous number repeats or alternates between calls. Equality against it either fires throughout ordinary play, shifting the key a frame every frame, or almost never fires and hides a real stall. Gylt and Silent Hill 2 repeat a number within a frame, The Outer Worlds 2 sends one per call, so no single title predicts which. A restart is told from extra view families by size: orders of magnitude against a few. The stall is decided where the number is read, not precomputed where it is published, so a game that stops running that path cannot leave a flag stuck at its last value. The threshold is 250ms of time, because "produced no frame" and "produces frames more slowly than we present" differ only in duration. The same stall breaks the native stereo fix, which takes the left eye from the game's backbuffer and the right from a scene capture UEVR drives. Behind a fullscreen menu the game stops drawing the world into its backbuffer while the capture keeps rendering one, so the right eye holds a frozen world while the left correctly holds the menu on black. Take both eyes from the backbuffer while stalled, as the null scene capture branch already does. Log both stall edges with the key, and log the pattern the game submits, once each. The game carries on rendering, so nothing else in the log changes when its number freezes.
1 parent 4ee5c6b commit b6b5746

3 files changed

Lines changed: 158 additions & 4 deletions

File tree

‎src/mods/VR.hpp‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,25 @@ class VR : public Mod {
550550
return m_native_stereo_fix_same_pass->value();
551551
}
552552

553+
// Called when the game's frame number exceeds the highest seen, which is the only reliable sign of a
554+
// new frame. Why the highest and not the previous one: begin_render_viewfamily.
555+
void notify_game_frame_advanced() {
556+
m_last_game_frame_advance = std::chrono::steady_clock::now();
557+
}
558+
559+
// True while the game has stopped producing frames, which some games do while a fullscreen menu is up.
560+
//
561+
// Answered here, at the point of use, rather than precomputed where the number is published: if the
562+
// game stops running that path altogether, a stored flag would be stuck at its last value.
563+
//
564+
// Some threshold is unavoidable, because "produced no frame" and "produces frames more slowly than we
565+
// present" differ only in duration, and time is the steadiest unit for it -- a count of presents would
566+
// depend on the game's frame rate against the headset's, a count of calls on how many view families
567+
// the game pushes per frame. 250ms outlasts any plausible slow frame and is invisible when a menu opens.
568+
bool is_game_frame_stalled() const {
569+
return (std::chrono::steady_clock::now() - m_last_game_frame_advance) >= std::chrono::milliseconds(250);
570+
}
571+
553572
bool is_ahud_compatibility_enabled() const {
554573
return m_compatibility_ahud->value();
555574
}
@@ -1136,6 +1155,9 @@ class VR : public Mod {
11361155
bool m_has_hw_scheduling{false}; // hardware accelerated GPU scheduling
11371156
bool m_spoofed_gamepad_connection{false};
11381157
bool m_aim_temp_disabled{false};
1158+
// Written where the view family begins, read while presenting. Unsynchronized on purpose, like the
1159+
// timestamps above it: a torn read costs one frame sourcing the wrong eye texture.
1160+
std::chrono::steady_clock::time_point m_last_game_frame_advance{std::chrono::steady_clock::now()};
11391161

11401162
struct {
11411163
bool draw{false};

‎src/mods/vr/D3D12Component.cpp‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,20 @@ vr::EVRCompositorError D3D12Component::on_frame(VR* vr) {
184184
.back = 1
185185
};
186186

187+
// A game that has stopped advancing its frame number has stopped drawing the world into its own
188+
// backbuffer, while the scene capture feeding the right eye keeps rendering one:
189+
//
190+
// left eye backbuffer -- menu only
191+
// right eye scene capture -- still the world
192+
//
193+
// Measured symptom: a frozen world in the right eye against a black left one, which reads as the
194+
// image rotating the wrong way when the head turns. Both eyes from the backbuffer while stalled,
195+
// so both show whatever the game is actually presenting.
196+
auto* const right_source = vr->is_game_frame_stalled() ? m_game_tex.texture.Get()
197+
: m_scene_capture_tex.texture.Get();
198+
187199
commands.copy_region_stereo(
188-
m_game_tex.texture.Get(), m_scene_capture_tex.texture.Get(), render_target,
200+
m_game_tex.texture.Get(), right_source, render_target,
189201
&left_src_box, &left_src_box,
190202
0, 0, 0, m_backbuffer_size[0] / 2, 0, 0,
191203
D3D12_RESOURCE_STATE_RENDER_TARGET,

‎src/mods/vr/FFakeStereoRenderingHook.cpp‎

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2596,6 +2596,12 @@ struct SceneViewExtensionAnalyzer {
25962596
static inline uint32_t pre_render_viewfamily_renderthread_index{0};
25972597
static inline uint32_t frame_count_offset{0};
25982598

2599+
// What turns the game's frame number into the key UEVR gives its pose pipeline. Maintained in
2600+
// begin_render_viewfamily on the game thread, added by everything that publishes a frame number, so
2601+
// the two publishers cannot disagree. Read from the render thread; relaxed because the cost of
2602+
// reading it a frame late is a frame, and the alternative is a lock on the hot path.
2603+
static inline std::atomic<uint32_t> frame_key_offset{};
2604+
25992605
template<int N>
26002606
static bool analysis_dummy_stage1(ISceneViewExtension* extension, uintptr_t a2, uintptr_t a3, uintptr_t a4) {
26012607
if (N == 0) {
@@ -3464,8 +3470,118 @@ void FFakeStereoRenderingHook::begin_render_viewfamily(ISceneViewExtension* exte
34643470

34653471
//vr->update_hmd_state(true, frame_count);
34663472
auto runtime = vr->get_runtime();
3467-
runtime->internal_frame_count = frame_count;
3468-
runtime->on_pre_render_game_thread(frame_count);
3473+
3474+
// The key UEVR gives its pose pipeline has to move forward, and only forward.
3475+
//
3476+
// Poses live in pipeline_states[key % QUEUE_SIZE], and OpenXR::on_pre_render_game_thread refreshes a
3477+
// slot only when it is empty or when the key jumped FORWARD past it. Any other revisit keeps the
3478+
// predicted display time already in the slot, so xrLocateViews predicts for a moment that has
3479+
// passed. On screen the image comes loose and swims against the head, faster the staler it is.
3480+
//
3481+
// The game's own number is not that key: it freezes while some games are paused, and it restarts
3482+
// from scratch on a level load. Both revisit slots out of order.
3483+
//
3484+
// number grows key grows with it, offset untouched
3485+
// number freezes key keeps moving on its own, so the slots keep rotating
3486+
// number restarts key carries on one above the highest it reached
3487+
// number repeats key stays put -- it is still the same frame
3488+
//
3489+
// Kept as an offset rather than a counter of our own, so a game that behaves stays on its own
3490+
// numbering and the offset stays zero.
3491+
//
3492+
// Progress is "exceeded the highest number seen", not "differs from the previous one". A game can
3493+
// push several view families per frame, and that breaks an equality test both ways:
3494+
//
3495+
// repeated numbers passes during ordinary play, shifting the key a frame every frame; the view
3496+
// answers head movement with amplified jitter. Seen in Gylt and Silent Hill 2
3497+
// alternating almost never passes, so a real stall goes unnoticed
3498+
//
3499+
// Which one a game shows is up to the game, so a single title proves nothing either way.
3500+
3501+
// A restart drops the number by orders of magnitude; extra view families differ by a few.
3502+
constexpr uint32_t restart_slack = 64;
3503+
3504+
static uint32_t highest_game_frame_count{};
3505+
static uint32_t key_carry{};
3506+
3507+
if (frame_count > highest_game_frame_count) {
3508+
highest_game_frame_count = frame_count;
3509+
vr->notify_game_frame_advanced();
3510+
} else if (frame_count + restart_slack < highest_game_frame_count) {
3511+
// One above the highest, so the key never falls back to the game's new number.
3512+
key_carry += highest_game_frame_count + 1 - frame_count;
3513+
3514+
SPDLOG_INFO("Game restarted its frame numbering ({} after a high of {}), carrying the key on to {}",
3515+
frame_count, highest_game_frame_count, frame_count + key_carry);
3516+
3517+
highest_game_frame_count = frame_count;
3518+
vr->notify_game_frame_advanced();
3519+
}
3520+
3521+
// Report which pattern the game submits, once each, because it is not visible any other way.
3522+
//
3523+
// A repeat only counts while the run stays short. A stalled game repeats its number on every call,
3524+
// so an unbounded test would report every stall as the very pattern it has to be told apart from:
3525+
// several view families make a run of several, a stall makes a run of hundreds. A restart is
3526+
// excluded from the alternating case for the same reason -- it also arrives below the previous
3527+
// number, and it is handled above.
3528+
constexpr uint32_t max_families_per_frame = 4;
3529+
3530+
static uint32_t previous_frame_count{};
3531+
static uint32_t repeat_run{};
3532+
3533+
if (frame_count == previous_frame_count) {
3534+
++repeat_run;
3535+
} else {
3536+
if (frame_count > previous_frame_count) {
3537+
if (repeat_run > 0 && repeat_run <= max_families_per_frame) {
3538+
SPDLOG_INFO_ONCE("Game submits repeated frame numbers within a frame (saw {} {} times in a row)",
3539+
previous_frame_count, repeat_run + 1);
3540+
}
3541+
} else if (previous_frame_count - frame_count <= restart_slack) {
3542+
SPDLOG_INFO_ONCE("Game alternates frame numbers per frame ({} after {})", frame_count, previous_frame_count);
3543+
}
3544+
3545+
repeat_run = 0;
3546+
}
3547+
3548+
previous_frame_count = frame_count;
3549+
3550+
// When a stall ends its frames are folded into the carry, not dropped: handing the key back is the
3551+
// same backwards jump a restart makes, and measured stalls reach 159 frames.
3552+
static uint32_t stall_frames{};
3553+
static bool was_stalled{};
3554+
3555+
const auto stalled = vr->is_game_frame_stalled();
3556+
const auto stalled_for = stall_frames;
3557+
3558+
if (stalled) {
3559+
++stall_frames;
3560+
} else if (stall_frames != 0) {
3561+
key_carry += stall_frames;
3562+
stall_frames = 0;
3563+
}
3564+
3565+
// Both edges, once per stall: the game keeps rendering, so nothing else in the log changes when its
3566+
// number freezes. The key is reported with them, so a log alone shows whether it went backwards.
3567+
if (stalled != was_stalled) {
3568+
was_stalled = stalled;
3569+
3570+
if (stalled) {
3571+
SPDLOG_INFO("Game stopped advancing its frame number, holding {}", frame_count);
3572+
} else {
3573+
SPDLOG_INFO("Game resumed advancing its frame number at {}, after {} frames of stall, key at {}",
3574+
frame_count, stalled_for, frame_count + key_carry);
3575+
}
3576+
}
3577+
3578+
const auto effective_frame_count = frame_count + key_carry + stall_frames;
3579+
3580+
// Published for the render thread, which adds it to the same game frame number.
3581+
SceneViewExtensionAnalyzer::frame_key_offset.store(key_carry + stall_frames, std::memory_order_relaxed);
3582+
3583+
runtime->internal_frame_count = effective_frame_count;
3584+
runtime->on_pre_render_game_thread(effective_frame_count);
34693585

34703586
// This is a HACKHACKHACK to get splitscreen working on around 4.20 to 4.27 something
34713587
// This is completely borked on UE5
@@ -3695,7 +3811,11 @@ void FFakeStereoRenderingHook::pre_render_viewfamily_renderthread(ISceneViewExte
36953811
cmd_list = *(sdk::FRHICommandListBase**)((uintptr_t)cmd_list + ue5_command_offset);
36963812
}
36973813

3698-
const auto compensation = g_hook->get_frame_delay_compensation();
3814+
// The same offset the game thread applied, so both publishers name a frame the same way. Without it
3815+
// the two disagree by the width of a stall or of a numbering restart, and a pose stored under one key
3816+
// is looked up under another.
3817+
const auto key_offset = (int32_t)SceneViewExtensionAnalyzer::frame_key_offset.load(std::memory_order_relaxed);
3818+
const auto compensation = g_hook->get_frame_delay_compensation() + key_offset;
36993819

37003820
// Using slate's draw window hook is the safest way to do this without
37013821
// false positives on the command list in this function

0 commit comments

Comments
 (0)