From df025d2eb7cf16065d0066eec815bfac45ba23f1 Mon Sep 17 00:00:00 2001 From: dim-ghub <59436063+dim-ghub@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:58:26 -0500 Subject: [PATCH] fix(vr): publish the shared-memory mirror a frame later instead of waiting With the eye copies no longer waited for, MOCKTAIL_VR_MIRROR_SHM made the CPU wait for the whole frame before publishing, which cut the frame rate from 117 to 82 fps at 120 Hz. Publish each readback at the next copy, when the GPU has finished it. The external viewer sees each frame one frame later. --- include/mocktail/vr/openxr_backend.h | 14 +++++++++ src/vr/openxr_backend.cc | 47 ++++++++++++++++------------ 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/include/mocktail/vr/openxr_backend.h b/include/mocktail/vr/openxr_backend.h index 6408ffa..c3df817 100644 --- a/include/mocktail/vr/openxr_backend.h +++ b/include/mocktail/vr/openxr_backend.h @@ -350,8 +350,21 @@ class OpenXrBackend final { VkCommandBuffer command = VK_NULL_HANDLE; VkFence fence = VK_NULL_HANDLE; bool pending = false; + std::uint64_t serial = 0; // bumped per submission std::vector images; }; + // The shared-memory mirror's readback from the previous frame copy. It is + // published at the next copy, when the GPU has long finished it, so neither + // the headset nor the mirror makes the CPU wait for the frame. + struct MirrorReadback { + AsyncSubmission* slot = nullptr; + std::uint64_t serial = 0; + std::uint64_t frame = 0; + VkFormat format = VK_FORMAT_UNDEFINED; + std::uint32_t width = 0; + std::uint32_t height = 0; + }; + void PublishMirrorReadbackLocked(); // The next slot, recorded-ready, or nullptr when its previous submission // did not finish in time (the device is hung or lost). AsyncSubmission* NextAsyncSubmissionLocked(); @@ -454,6 +467,7 @@ class OpenXrBackend final { static constexpr std::size_t kAsyncSubmissions = 6; std::array async_{}; std::size_t next_async_ = 0; + MirrorReadback mirror_readback_{}; // The mirror's signal for this present, which PrepareHostPresent passes on. VkSemaphore mirror_ready_ = VK_NULL_HANDLE; // Semaphores of destroyed desktop swapchains. A present may still wait on diff --git a/src/vr/openxr_backend.cc b/src/vr/openxr_backend.cc index 9b5f8a0..f2f118c 100644 --- a/src/vr/openxr_backend.cc +++ b/src/vr/openxr_backend.cc @@ -3262,6 +3262,7 @@ bool OpenXrBackend::EnsureCopyResources(VkDevice device, void OpenXrBackend::DestroyCopyResources() { mirror_ready_ = VK_NULL_HANDLE; + mirror_readback_ = {}; next_async_ = 0; if (vk_ == nullptr || vk_device_ == VK_NULL_HANDLE) { copy_pool_ = VK_NULL_HANDLE; @@ -3319,6 +3320,7 @@ VkResult OpenXrBackend::SubmitAsyncLocked(VkQueue queue, AsyncSubmission *slot, slot->fence); } perf::ProcessCollector().Count(perf::Counter::kSubmits); + ++slot->serial; slot->pending = result == VK_SUCCESS; if (!slot->pending) slot->images.clear(); @@ -3406,6 +3408,8 @@ bool OpenXrBackend::CopyEyesIntoSwapchains(VkQueue queue) { const VkImage destination[2] = { eye_swapchains_[0].images[eye_swapchains_[0].acquired_index], eye_swapchains_[1].images[eye_swapchains_[1].acquired_index]}; + // Publish the last readback before its staging is resized or overwritten. + PublishMirrorReadbackLocked(); EnsureMirrorForExtent(eyes_[0].width, eyes_[0].height); AsyncSubmission *const slot = NextAsyncSubmissionLocked(); @@ -3538,29 +3542,32 @@ bool OpenXrBackend::CopyEyesIntoSwapchains(VkQueue queue) { if (SubmitAsyncLocked(queue, slot, submit) != VK_SUCCESS) { return false; } - if (mirror_.enabled()) { - // The shared-memory readback must land before it is published. - VkResult fence_result = VK_SUCCESS; - { - const ScopedStageTimer fence_timer(perf::Stage::kCopyFenceWait); - fence_result = vk_->WaitForFences(vk_device_, 1, &slot->fence, VK_TRUE, - 2'000'000'000ULL); - } - perf::ProcessCollector().Count(perf::Counter::kFenceWaits); - if (fence_result != VK_SUCCESS) { - (void)IdleHostDevice(vk_->DeviceWaitIdle, vk_device_); - throw std::runtime_error("XR copy fence wait failed"); - } - slot->pending = false; - slot->images.clear(); - const ScopedStageTimer publish_timer(perf::Stage::kMirrorPublish); - mirror_.Publish(published_pose_.frame, eyes_[0].format, eyes_[0].width, - eyes_[0].height, perf::NowNs()); - perf::ProcessCollector().Count(perf::Counter::kMirrorPairs); - } + if (mirror_.enabled()) + mirror_readback_ = {slot, slot->serial, published_pose_.frame, + eyes_[0].format, eyes_[0].width, eyes_[0].height}; return true; } +void OpenXrBackend::PublishMirrorReadbackLocked() { + const MirrorReadback readback = std::exchange(mirror_readback_, {}); + if (readback.slot == nullptr || !mirror_.enabled()) + return; + // A reused slot was waited for before reuse, so its readback landed. + if (readback.slot->serial == readback.serial && readback.slot->pending) { + const ScopedStageTimer fence_timer(perf::Stage::kCopyFenceWait); + perf::ProcessCollector().Count(perf::Counter::kFenceWaits); + if (vk_->WaitForFences(vk_device_, 1, &readback.slot->fence, VK_TRUE, + 2'000'000'000ULL) != VK_SUCCESS) { + perf::ProcessCollector().Count(perf::Counter::kMirrorDrops); + return; + } + } + const ScopedStageTimer publish_timer(perf::Stage::kMirrorPublish); + mirror_.Publish(readback.frame, readback.format, readback.width, + readback.height, perf::NowNs()); + perf::ProcessCollector().Count(perf::Counter::kMirrorPairs); +} + void OpenXrBackend::EnsureMirrorForExtent(std::uint32_t width, std::uint32_t height) { static const std::string name = EnvString("MOCKTAIL_VR_MIRROR_SHM", "");