From c5377badd9f1a678dc42fd69b2d47aaec02132ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:17:24 +0000 Subject: [PATCH 1/4] Initial plan From 4bfa80030c9bb7822c554ddee3a3368428b3852e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:22:32 +0000 Subject: [PATCH 2/4] Add OpenVR non-synced mode (AFR) with runtime reprojection Implement Alternate Frame Rendering for OpenVR, matching OpenXR behavior: - Each eye submits with the pose from when it was actually rendered - Both eyes are submitted each present (one new, one resubmitted) - Runtime handles reprojection based on pose delta - Skip resubmission on first frame when no previous frame exists Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- src/mods/vr/D3D11Component.cpp | 62 ++++++++++++++++++++++++---- src/mods/vr/D3D12Component.cpp | 75 ++++++++++++++++++++++++++++++++-- 2 files changed, 126 insertions(+), 11 deletions(-) diff --git a/src/mods/vr/D3D11Component.cpp b/src/mods/vr/D3D11Component.cpp index 25b0a7c..51a11ba 100644 --- a/src/mods/vr/D3D11Component.cpp +++ b/src/mods/vr/D3D11Component.cpp @@ -302,21 +302,46 @@ vr::EVRCompositorError D3D11Component::on_frame(VR* vr) { } if (runtime->is_openvr()) { + // Calculate which frame each eye was actually rendered at (matching OpenXR's AFR logic) + auto l_frame = vr->m_presenter_frame_count % 2 == vr->m_left_eye_interval + ? vr->m_presenter_frame_count + : vr->m_presenter_frame_count - 1; + auto r_frame = vr->m_presenter_frame_count % 2 == vr->m_left_eye_interval + ? vr->m_presenter_frame_count - 1 + : vr->m_presenter_frame_count; + + // Submit newly rendered left eye with its pose vr::VRTextureWithPose_t left_eye{}; left_eye.handle = (void*)m_left_eye_tex.Get(); left_eye.eType = vr::TextureType_DirectX; left_eye.eColorSpace = vr::ColorSpace_Auto; - left_eye.mDeviceToAbsoluteTracking = GlobalPool::get_openvr_pose(vr->m_presenter_frame_count); + left_eye.mDeviceToAbsoluteTracking = GlobalPool::get_openvr_pose(l_frame); auto e = vr::VRCompositor()->Submit(vr::Eye_Left, (vr::Texture_t*)&left_eye, &vr->m_left_bounds, vr::Submit_TextureWithPose); - bool submitted = true; - if (e != vr::VRCompositorError_None) { spdlog::error("[VR] VRCompositor failed to submit left eye: {}", (int)e); vr->m_submitted = false; return e; } + + // Resubmit previous right eye frame with its original pose for runtime reprojection + // Skip on first frame when there's no previous frame to resubmit + if (vr->m_presenter_frame_count > 0) { + vr::VRTextureWithPose_t right_eye{}; + right_eye.handle = (void*)m_right_eye_tex.Get(); + right_eye.eType = vr::TextureType_DirectX; + right_eye.eColorSpace = vr::ColorSpace_Auto; + right_eye.mDeviceToAbsoluteTracking = GlobalPool::get_openvr_pose(r_frame); + + e = vr::VRCompositor()->Submit(vr::Eye_Right, (vr::Texture_t*)&right_eye, &vr->m_right_bounds, vr::Submit_TextureWithPose); + + if (e != vr::VRCompositorError_None) { + spdlog::error("[VR] VRCompositor failed to submit right eye (resubmit): {}", (int)e); + } + } + + vr->m_submitted = true; } } else { auto copy_from_tex = backbufferIs8Bit ? backbuffer : m_right_eye_rt.tex; @@ -394,23 +419,46 @@ vr::EVRCompositorError D3D11Component::on_frame(VR* vr) { if (backbufferIs8Bit) { context->CopyResource(m_right_eye_tex.Get(), backbuffer.Get()); } - + + // Calculate which frame each eye was actually rendered at (matching OpenXR's AFR logic) + auto l_frame = vr->m_presenter_frame_count % 2 == vr->m_left_eye_interval + ? vr->m_presenter_frame_count + : vr->m_presenter_frame_count - 1; + auto r_frame = vr->m_presenter_frame_count % 2 == vr->m_left_eye_interval + ? vr->m_presenter_frame_count - 1 + : vr->m_presenter_frame_count; + + // Submit newly rendered right eye with its pose vr::VRTextureWithPose_t right_eye{}; right_eye.handle = (void*)m_right_eye_tex.Get(); right_eye.eType = vr::TextureType_DirectX; right_eye.eColorSpace = vr::ColorSpace_Auto; - right_eye.mDeviceToAbsoluteTracking = GlobalPool::get_openvr_pose(vr->m_presenter_frame_count); + right_eye.mDeviceToAbsoluteTracking = GlobalPool::get_openvr_pose(r_frame); auto e = vr::VRCompositor()->Submit(vr::Eye_Right, (vr::Texture_t*)&right_eye, &vr->m_right_bounds, vr::Submit_TextureWithPose); - bool submitted = true; - if (e != vr::VRCompositorError_None) { spdlog::error("[VR] VRCompositor failed to submit right eye: {}", (int)e); vr->m_submitted = false; return e; } + // Resubmit previous left eye frame with its original pose for runtime reprojection + // Skip on first frame when there's no previous frame to resubmit + if (vr->m_presenter_frame_count > 0) { + vr::VRTextureWithPose_t left_eye{}; + left_eye.handle = (void*)m_left_eye_tex.Get(); + left_eye.eType = vr::TextureType_DirectX; + left_eye.eColorSpace = vr::ColorSpace_Auto; + left_eye.mDeviceToAbsoluteTracking = GlobalPool::get_openvr_pose(l_frame); + + e = vr::VRCompositor()->Submit(vr::Eye_Left, (vr::Texture_t*)&left_eye, &vr->m_left_bounds, vr::Submit_TextureWithPose); + + if (e != vr::VRCompositorError_None) { + spdlog::error("[VR] VRCompositor failed to submit left eye (resubmit): {}", (int)e); + } + } + vr->m_submitted = true; } diff --git a/src/mods/vr/D3D12Component.cpp b/src/mods/vr/D3D12Component.cpp index 155c3bf..93c6905 100644 --- a/src/mods/vr/D3D12Component.cpp +++ b/src/mods/vr/D3D12Component.cpp @@ -97,17 +97,26 @@ vr::EVRCompositorError D3D12Component::on_frame(VR* vr) { if (runtime->is_openvr()) { m_openvr.copy_left(eye_texture.Get()); + // Calculate which frame each eye was actually rendered at (matching OpenXR's AFR logic) + auto l_frame = vr->m_presenter_frame_count % 2 == vr->m_left_eye_interval + ? vr->m_presenter_frame_count + : vr->m_presenter_frame_count - 1; + auto r_frame = vr->m_presenter_frame_count % 2 == vr->m_left_eye_interval + ? vr->m_presenter_frame_count - 1 + : vr->m_presenter_frame_count; + vr::D3D12TextureData_t left_tex { m_openvr.get_left().texture.Get(), command_queue, 0 }; + // Submit newly rendered left eye with its pose vr::VRTextureWithPose_t left_eye{}; left_eye.handle = (void*)&left_tex; left_eye.eType = vr::TextureType_DirectX12; left_eye.eColorSpace = vr::ColorSpace_Auto; - left_eye.mDeviceToAbsoluteTracking = GlobalPool::get_openvr_pose(vr->m_presenter_frame_count); + left_eye.mDeviceToAbsoluteTracking = GlobalPool::get_openvr_pose(l_frame); const auto left_bounds = vr::VRTextureBounds_t{runtime->view_bounds[0][0], runtime->view_bounds[0][2], runtime->view_bounds[0][1], runtime->view_bounds[0][3]}; @@ -117,6 +126,32 @@ vr::EVRCompositorError D3D12Component::on_frame(VR* vr) { spdlog::error("[VR] VRCompositor failed to submit left eye: {}", (int)e); return e; } + + // Resubmit previous right eye frame with its original pose for runtime reprojection + // Skip on first frame when there's no previous frame to resubmit + if (vr->m_presenter_frame_count > 0) { + vr::D3D12TextureData_t right_tex { + m_openvr.get_right().texture.Get(), + command_queue, + 0 + }; + + vr::VRTextureWithPose_t right_eye{}; + right_eye.handle = (void*)&right_tex; + right_eye.eType = vr::TextureType_DirectX12; + right_eye.eColorSpace = vr::ColorSpace_Auto; + right_eye.mDeviceToAbsoluteTracking = GlobalPool::get_openvr_pose(r_frame); + + const auto right_bounds = vr::VRTextureBounds_t{runtime->view_bounds[1][0], runtime->view_bounds[1][2], + runtime->view_bounds[1][1], runtime->view_bounds[1][3]}; + e = vr::VRCompositor()->Submit(vr::Eye_Right, (vr::Texture_t*)&right_eye, &right_bounds, vr::Submit_TextureWithPose); + + if (e != vr::VRCompositorError_None) { + spdlog::error("[VR] VRCompositor failed to submit right eye (resubmit): {}", (int)e); + } + } + + vr->m_submitted = true; } } else { // OpenXR texture @@ -140,17 +175,26 @@ vr::EVRCompositorError D3D12Component::on_frame(VR* vr) { if (runtime->is_openvr()) { m_openvr.copy_right(eye_texture.Get()); + // Calculate which frame each eye was actually rendered at (matching OpenXR's AFR logic) + auto l_frame = vr->m_presenter_frame_count % 2 == vr->m_left_eye_interval + ? vr->m_presenter_frame_count + : vr->m_presenter_frame_count - 1; + auto r_frame = vr->m_presenter_frame_count % 2 == vr->m_left_eye_interval + ? vr->m_presenter_frame_count - 1 + : vr->m_presenter_frame_count; + vr::D3D12TextureData_t right_tex { m_openvr.get_right().texture.Get(), command_queue, 0 }; + // Submit newly rendered right eye with its pose vr::VRTextureWithPose_t right_eye{}; right_eye.handle = (void*)&right_tex; right_eye.eType = vr::TextureType_DirectX12; right_eye.eColorSpace = vr::ColorSpace_Auto; - right_eye.mDeviceToAbsoluteTracking = GlobalPool::get_openvr_pose(vr->m_presenter_frame_count); + right_eye.mDeviceToAbsoluteTracking = GlobalPool::get_openvr_pose(r_frame); const auto right_bounds = vr::VRTextureBounds_t{runtime->view_bounds[1][0], runtime->view_bounds[1][2], runtime->view_bounds[1][1], runtime->view_bounds[1][3]}; @@ -159,10 +203,33 @@ vr::EVRCompositorError D3D12Component::on_frame(VR* vr) { if (e != vr::VRCompositorError_None) { spdlog::error("[VR] VRCompositor failed to submit right eye: {}", (int)e); return e; - } else { - vr->m_submitted = true; } + // Resubmit previous left eye frame with its original pose for runtime reprojection + // Skip on first frame when there's no previous frame to resubmit + if (vr->m_presenter_frame_count > 0) { + vr::D3D12TextureData_t left_tex { + m_openvr.get_left().texture.Get(), + command_queue, + 0 + }; + + vr::VRTextureWithPose_t left_eye{}; + left_eye.handle = (void*)&left_tex; + left_eye.eType = vr::TextureType_DirectX12; + left_eye.eColorSpace = vr::ColorSpace_Auto; + left_eye.mDeviceToAbsoluteTracking = GlobalPool::get_openvr_pose(l_frame); + + const auto left_bounds = vr::VRTextureBounds_t{runtime->view_bounds[0][0], runtime->view_bounds[0][2], + runtime->view_bounds[0][1], runtime->view_bounds[0][3]}; + e = vr::VRCompositor()->Submit(vr::Eye_Left, (vr::Texture_t*)&left_eye, &left_bounds, vr::Submit_TextureWithPose); + + if (e != vr::VRCompositorError_None) { + spdlog::error("[VR] VRCompositor failed to submit left eye (resubmit): {}", (int)e); + } + } + + vr->m_submitted = true; ++m_openvr.texture_counter; } } From 6e92f3d5edff989d939ec7c18b882b933d4f0c92 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:11:43 +0000 Subject: [PATCH 3/4] Move async_aer to runtime level and make it configurable for both OpenXR and OpenVR - Move async_aer from OpenXR.hpp to VRRuntime.hpp base class - Update is_using_async_aer() to check both user setting and runtime capability - AFR now works for both OpenXR and OpenVR runtimes Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- src/mods/VR.hpp | 2 +- src/mods/vr/runtimes/OpenXR.hpp | 1 - src/mods/vr/runtimes/VRRuntime.hpp | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mods/VR.hpp b/src/mods/VR.hpp index 8a56940..4f814b4 100644 --- a/src/mods/VR.hpp +++ b/src/mods/VR.hpp @@ -136,7 +136,7 @@ class VR : public Mod { // int32_t get_game_frame_count() const; bool is_using_async_aer() const { - return m_runtime->is_openxr() && m_use_async_aer->value(); + return m_use_async_aer->value() && m_runtime->async_aer; } bool is_gui_enabled() const { diff --git a/src/mods/vr/runtimes/OpenXR.hpp b/src/mods/vr/runtimes/OpenXR.hpp index 773d530..3fc1ec7 100644 --- a/src/mods/vr/runtimes/OpenXR.hpp +++ b/src/mods/vr/runtimes/OpenXR.hpp @@ -176,7 +176,6 @@ struct OpenXR final : public VRRuntime { bool session_ready{false}; bool frame_began{false}; bool frame_synced{false}; - bool async_aer{true}; #ifdef DEBUG_PROFILING_ENABLED bool profile_calls{true}; #else diff --git a/src/mods/vr/runtimes/VRRuntime.hpp b/src/mods/vr/runtimes/VRRuntime.hpp index c2592dc..cedc7bd 100644 --- a/src/mods/vr/runtimes/VRRuntime.hpp +++ b/src/mods/vr/runtimes/VRRuntime.hpp @@ -132,6 +132,7 @@ struct VRRuntime { bool got_first_sync{false}; bool handle_pause{false}; bool wants_reset_origin{true}; + bool async_aer{true}; // Alternate Frame Rendering - submit both eyes each present with historical poses std::optional error{}; From cb1ba6d79b44df8dcdd896972c91a5cde40552c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:27:16 +0000 Subject: [PATCH 4/4] Simplify is_using_async_aer() to rely only on runtime's async_aer - is_using_async_aer() now only checks m_runtime->async_aer - Set runtime's async_aer from config on load - Sync runtime's async_aer when UI toggle changes Co-authored-by: mutars <4204406+mutars@users.noreply.github.com> --- src/mods/VR.cpp | 4 ++++ src/mods/VR.hpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mods/VR.cpp b/src/mods/VR.cpp index 1053874..f45d15c 100644 --- a/src/mods/VR.cpp +++ b/src/mods/VR.cpp @@ -1160,6 +1160,9 @@ void VR::on_draw_ui() { m_openxr->resolution_scale = m_resolution_scale->value(); } m_use_async_aer->draw("Use Async AER"); + if (ImGui::IsItemDeactivatedAfterEdit()) { + get_runtime()->async_aer = m_use_async_aer->value(); + } m_flat_screen_distance->draw("Flat Screen Distance"); if (ImGui::IsItemDeactivatedAfterEdit()) @@ -1306,6 +1309,7 @@ void VR::on_config_load(const utility::Config& cfg, bool set_defaults) { get_runtime()->m_vertical_fov_scale = m_vertical_fov_scale->value(); get_runtime()->m_extended_fov_range = m_extended_fov_rage->value(); get_runtime()->m_flat_screen_distance = m_flat_screen_distance->value(); + get_runtime()->async_aer = m_use_async_aer->value(); } m_overlay_component.on_config_load(cfg, set_defaults); diff --git a/src/mods/VR.hpp b/src/mods/VR.hpp index 4f814b4..ea95ad7 100644 --- a/src/mods/VR.hpp +++ b/src/mods/VR.hpp @@ -136,7 +136,7 @@ class VR : public Mod { // int32_t get_game_frame_count() const; bool is_using_async_aer() const { - return m_use_async_aer->value() && m_runtime->async_aer; + return m_runtime->async_aer; } bool is_gui_enabled() const {