From fbc9ce721728712c5ef9011e02ed977e268f2231 Mon Sep 17 00:00:00 2001 From: iChris4 Date: Sun, 20 Sep 2026 17:36:08 +0200 Subject: [PATCH] Implement render-first pacing for D3D12 and Vulkan backends; enhance packet management and diagnostics --- OPENXR.md | 37 ++-- runtime/include/vr/openxr_d3d12.h | 10 ++ runtime/src/vr/openxr_d3d12.cpp | 180 +++++++++++++++++++- runtime/src/vr/openxr_integration.cpp | 26 ++- runtime/tests/openxr_d3d12_replay_tests.cpp | 150 +++++++++++++++- 5 files changed, 377 insertions(+), 26 deletions(-) diff --git a/OPENXR.md b/OPENXR.md index 18261cb..14e35f6 100644 --- a/OPENXR.md +++ b/OPENXR.md @@ -320,18 +320,31 @@ short-lived immutable stereo packet. Each sealed GX frame and immersive packet c policy-generation tag; a mismatch is rendered in mono and the acquired XR frame is canceled, so an asynchronous menu/race transition cannot replay race transforms over unsafe content. -With interpolation off, the standalone (Vulkan) backend paces render-first: the pacing thread -locates the views for an estimated display time (two periods past the last one the compositor -predicted), hands Aurora the packet and its shared-buffer targets with no compositor frame open, -and waits for Aurora to render the eyes at its next seal, repeating the retained layer if that -takes more than 50 ms. Only then does it call xrWaitFrame and xrBeginFrame, copy the eyes into the -freshly acquired swapchain images and end the frame with the packet's render poses. A headset -frame therefore stays open for the copy alone instead of for the next 60 Hz game frame plus the -whole encode, which on a 72 or 90 Hz display used to make every second cycle span two display -slots (about 45 headset frames per second while the game rendered 60). The compositor reprojects -the rendered pose to the frame it lands in. The D3D12 backend keeps the frame-first order below, -as does interpolation on either backend, since interpolation renders for the frame's own -predicted display time. +With interpolation off, both PC (D3D12) and standalone (Vulkan) pace render-first: +the pacing thread locates views for an estimated display time (two periods past the last +prediction), hands Aurora a packet without leaving a compositor frame open, and waits for +rendering. A 50 ms stall repeats the retained layer; cancellation also advances a keep-alive +cycle to refresh timing. Once rendering is submitted, the thread calls xrWaitFrame and +xrBeginFrame, completes backend-specific copy/release work, and ends the frame using the +packet's original render poses with the current compositor display time. + +Vulkan renders into shared buffers and copies them into newly acquired XR images afterward. +D3D12 acquires images from its non-retained swapchain pair before rendering; Aurora queues the +copy on the session's D3D12 queue before reporting completion. PC therefore needs no additional +copy in the short compositor cycle. Pending images remain acquired and separate from the +retained pair until completion or confirmed cancellation before encoding. GPU failure still +requires the existing queue-drain teardown. Rendered poses keep the session/reference-space +serials recorded when the packet was prepared, so changes during rendering invalidate them. + +VR interpolation keeps the frame-first order on both backends because it renders for the +frame's own predicted display time. The log announces `OpenXR D3D12 pacing: render-first` or +`frame-first (VR interpolation)` on each transition. For PC testing, disable **VR** frame +interpolation for a race capture; changing desktop interpolation alone does not select this +path. Menus use render-first even when VR interpolation is configured for races. Compare the +new diagnostic `open`, `end-gap`, `late`, and stage timings against a frame-first capture on +the same course and settings. Shorter `open` alone does not prove fewer black frames: rendering +and xrWaitFrame still take time outside that interval. Hardware testing is needed to measure +latency, runtime throttling and visible blackouts. With VR interpolation enabled, Aurora retains each sealed race's command stream and matched previous/current transform uniforms. New OpenXR packets wake the frame worker between game diff --git a/runtime/include/vr/openxr_d3d12.h b/runtime/include/vr/openxr_d3d12.h index a6401cb..51d6845 100644 --- a/runtime/include/vr/openxr_d3d12.h +++ b/runtime/include/vr/openxr_d3d12.h @@ -81,6 +81,16 @@ public: // available, resubmits the retained layer using the current display time. bool FinishFrame(OpenXRD3D12Frame& frame, bool submit_layer); + // Render-first path when interpolation is off. Aurora renders into acquired + // non-retained XR images while no compositor frame is open. BeginFrameForPacket + // accepts only a completed packet; CopyRenderedEyes verifies the already queued + // bridge copy. FinishFrame releases the images and submits their original poses. + OpenXRBeginStatus PreparePacket(const OpenXRPresentation& presentation, OpenXRBackendFrame& packet); + bool TryCancelPendingPacket(OpenXRBackendFrame& packet); + OpenXRBeginStatus BeginFrameForPacket(const OpenXRBackendFrame& packet, OpenXRBackendFrame& frame); + OpenXRSubmissionStatus CopyRenderedEyes(const OpenXRBackendFrame& frame); + OpenXRBeginStatus KeepAliveCycle(); + // Call on the XR owner thread after Aurora's worker is idle and before // aurora_shutdown(). Safe to repeat. False means a submitted D3D12 command // could not be fenced; the caller must retain this backend and its runtime diff --git a/runtime/src/vr/openxr_d3d12.cpp b/runtime/src/vr/openxr_d3d12.cpp index 1100da4..f6595c4 100644 --- a/runtime/src/vr/openxr_d3d12.cpp +++ b/runtime/src/vr/openxr_d3d12.cpp @@ -234,7 +234,7 @@ public: Fail("BeginFrame called before the D3D12 backend was bound"); return OpenXRD3D12BeginStatus::Error; } - if (frame_active_) { + if (frame_active_ || pending_packet_serial_ != 0) { Fail("BeginFrame called while another OpenXR frame is active"); return OpenXRD3D12BeginStatus::Error; } @@ -255,6 +255,7 @@ public: break; } } + NoteDisplayTiming(frame.xr_frame); if (!runtime_->BeginFrame(frame.xr_frame)) { Fail("xrBeginFrame failed"); return OpenXRD3D12BeginStatus::Error; @@ -283,8 +284,13 @@ public: return OpenXRD3D12BeginStatus::Ready; } + return PrepareTargets(frame); + } + + // Both pacing paths retain ownership until completion or cancellation. + OpenXRD3D12BeginStatus PrepareTargets(OpenXRD3D12Frame& frame) { const uint32_t target_count = - presentation.mode == OpenXRD3D12FrameMode::VirtualScreen ? 1u : kOpenXREyeCount; + frame.presentation.mode == OpenXRD3D12FrameMode::VirtualScreen ? 1u : kOpenXREyeCount; if (target_count == 1) { frame.render_width[1] = frame.render_width[0]; frame.render_height[1] = frame.render_height[0]; @@ -332,6 +338,151 @@ public: return OpenXRD3D12BeginStatus::Ready; } + // D3D12 renders straight into the non-retained XR swapchain pair. Acquiring + // images is independent of the compositor cycle; keep them acquired while + // Aurora owns them, and never expose them through the retained pair early. + OpenXRBeginStatus PreparePacket(const OpenXRPresentation& presentation, OpenXRBackendFrame& packet) { + packet = {}; + packet.presentation = presentation; + if (!bound_ || runtime_ == nullptr || frame_active_ || pending_packet_serial_ != 0) { + Fail("PreparePacket called before binding or with work pending"); + return OpenXRBeginStatus::Error; + } + if (runtime_->ShouldExit()) return OpenXRBeginStatus::ExitRequested; + if (!runtime_->IsSessionRunning()) return OpenXRBeginStatus::SessionNotRunning; + if (timing_session_serial_ != runtime_->SessionRunSerial() || last_display_period_ <= 0) { + const auto status = KeepAliveCycle(); + if (status != OpenXRBeginStatus::Ready) return status; + } + packet.xr_frame.serial = next_packet_serial_++; + packet.xr_frame.predicted_display_time = last_display_time_ + 2 * last_display_period_; + packet.xr_frame.predicted_display_period = last_display_period_; + packet.xr_frame.should_render = last_should_render_; + for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) { + packet.render_width[eye] = eye_swapchains_[eye].width; + packet.render_height[eye] = eye_swapchains_[eye].height; + } + if (!packet.xr_frame.should_render) return OpenXRBeginStatus::Ready; + if (!runtime_->LocateViewsAt(packet.xr_frame.predicted_display_time, packet.xr_frame)) { + Fail("xrLocateViews failed for a D3D12 packet"); + return OpenXRBeginStatus::Error; + } + if (!packet.xr_frame.views_valid) return OpenXRBeginStatus::Ready; + render_session_serial_ = runtime_->SessionRunSerial(); + render_space_serial_ = runtime_->LastReferenceSpaceChange().serial; + const auto status = PrepareTargets(packet); + if (status == OpenXRBeginStatus::Ready) pending_packet_serial_ = packet.xr_frame.serial; + return status; + } + + bool TryCancelPendingPacket(OpenXRBackendFrame& packet) { + if (frame_active_ || pending_packet_serial_ == 0 || + packet.xr_frame.serial != pending_packet_serial_ || !packet.expects_gpu_submission || + !aurora_d3d12_cancel_stereo_targets(packet.xr_frame.serial)) return false; + { + std::lock_guard lock(submission_mutex_); + awaiting_token_ = submitted_token_ = 0; + submission_arrived_ = submission_success_ = submission_unsafe_ = false; + } + packet.expects_gpu_submission = false; + pending_packet_serial_ = 0; + const diagnostics::Stopwatch release_timer; + const bool released = ReleaseAcquiredSwapchains(); + diagnostics::OnSwapchainRelease(release_timer); + // A release error is fatal; keep it visible to the next prepare rather + // than letting it register new targets over still-acquired images. + if (!released) pending_packet_serial_ = packet.xr_frame.serial; + return true; // Encoding was canceled; a release error blocks the next prepare. + } + + void NoteDisplayTiming(const OpenXRFrame& frame) { + last_display_time_ = frame.predicted_display_time; + last_display_period_ = frame.predicted_display_period; + last_should_render_ = frame.should_render; + timing_session_serial_ = runtime_->SessionRunSerial(); + } + + OpenXRBeginStatus BeginCompositorCycle() { + const auto status = runtime_->WaitFrame(active_frame_); + if (status != OpenXRFrameStatus::Ready) { + if (status == OpenXRFrameStatus::Error) Fail(BeginStatusOperation(status)); + return status == OpenXRFrameStatus::SessionNotRunning ? OpenXRBeginStatus::SessionNotRunning + : status == OpenXRFrameStatus::ExitRequested ? OpenXRBeginStatus::ExitRequested + : OpenXRBeginStatus::Error; + } + NoteDisplayTiming(active_frame_); + if (!runtime_->BeginFrame(active_frame_)) { + Fail("xrBeginFrame failed for a D3D12 compositor cycle"); + return OpenXRBeginStatus::Error; + } + frame_active_ = true; + return OpenXRBeginStatus::Ready; + } + + OpenXRBeginStatus KeepAliveCycle() { + if (!bound_ || runtime_ == nullptr || frame_active_) { + Fail("KeepAliveCycle called before binding or with an active frame"); + return OpenXRBeginStatus::Error; + } + const auto status = BeginCompositorCycle(); + if (status != OpenXRBeginStatus::Ready) return status; + const bool ended = EndRetainedFrame(false); + frame_active_ = false; + active_frame_ = {}; + if (!ended) { + Fail("OpenXR could not resubmit the retained D3D12 frame"); + return OpenXRBeginStatus::Error; + } + return OpenXRBeginStatus::Ready; + } + + OpenXRBeginStatus BeginFrameForPacket(const OpenXRBackendFrame& packet, OpenXRBackendFrame& frame) { + frame = {}; + if (!bound_ || runtime_ == nullptr || frame_active_ || pending_packet_serial_ == 0 || + packet.xr_frame.serial != pending_packet_serial_ || !packet.expects_gpu_submission || + WaitForSubmission(packet, 0) != OpenXRSubmissionStatus::Success) { + Fail("BeginFrameForPacket requires the completed current D3D12 packet"); + return OpenXRBeginStatus::Error; + } + const auto status = BeginCompositorCycle(); + if (status != OpenXRBeginStatus::Ready) { + // Completion was already confirmed. A stopped session must not + // strand a packet and block preparation after the next READY event. + if (status == OpenXRBeginStatus::SessionNotRunning) { + const bool released = ReleaseAcquiredSwapchains(); + pending_packet_serial_ = 0; + std::lock_guard lock(submission_mutex_); + awaiting_token_ = submitted_token_ = 0; + submission_arrived_ = submission_success_ = submission_unsafe_ = false; + if (!released) return OpenXRBeginStatus::Error; + } + return status; + } + frame = packet; + // Use the current compositor token/time but the original render poses. + frame.xr_frame.serial = active_frame_.serial; + frame.xr_frame.predicted_display_time = active_frame_.predicted_display_time; + frame.xr_frame.predicted_display_period = active_frame_.predicted_display_period; + frame.xr_frame.should_render = active_frame_.should_render; + active_frame_serial_ = frame.xr_frame.serial; + { + std::lock_guard lock(submission_mutex_); + awaiting_token_ = submitted_token_ = frame.xr_frame.serial; + } + pending_packet_serial_ = 0; + return OpenXRBeginStatus::Ready; + } + + OpenXRSubmissionStatus CopyRenderedEyes(const OpenXRBackendFrame& frame) { + if (!frame_active_ || frame.xr_frame.serial != active_frame_serial_) { + Fail("CopyRenderedEyes received a stale D3D12 frame"); + return OpenXRSubmissionStatus::Failed; + } + // The native bridge already queued the copy on the session's D3D12 + // queue before publishing completion. There is no second copy on PC. + return WaitForSubmission(frame, 0); + } + OpenXRD3D12SubmissionStatus WaitForSubmission(const OpenXRD3D12Frame& frame, uint32_t timeout_ms) { if (!frame.expects_gpu_submission) { @@ -456,6 +607,7 @@ public: !runtime_->BeginFrame(active_frame_)) { return Fail("OpenXR could not start a retained-frame compositor cycle"); } + NoteDisplayTiming(active_frame_); frame_active_ = true; return true; } @@ -577,6 +729,8 @@ public: active_frame_ = {}; } DestroySwapchains(); + pending_packet_serial_ = 0; + last_display_period_ = 0; if (owns_session_ && runtime_ != nullptr) { runtime_->DestroySession(); owns_session_ = false; @@ -779,7 +933,7 @@ private: } void EndActiveFrameWithoutLayers(const OpenXRFrame& frame) { - if (runtime_ != nullptr && runtime_->IsSessionRunning()) { + if (frame_active_ && runtime_ != nullptr && runtime_->IsSessionRunning()) { runtime_->EndFrameWithoutLayers(frame); } frame_active_ = false; @@ -851,6 +1005,12 @@ private: bool submission_unsafe_ = false; bool shutting_down_ = false; + uint64_t pending_packet_serial_ = 0; + uint64_t next_packet_serial_ = 1ull << 40; + uint64_t timing_session_serial_ = 0; + XrTime last_display_time_ = 0; + XrDuration last_display_period_ = 0; + bool last_should_render_ = false; uint64_t active_frame_serial_ = 0; uint64_t render_session_serial_ = 0; uint64_t render_space_serial_ = 0; @@ -898,6 +1058,20 @@ bool OpenXRD3D12Backend::RepeatFrame(const OpenXRD3D12Frame& frame) { return m_impl->RepeatFrame(frame); } +OpenXRBeginStatus OpenXRD3D12Backend::PreparePacket(const OpenXRPresentation& presentation, OpenXRBackendFrame& packet) { + return m_impl->PreparePacket(presentation, packet); +} +bool OpenXRD3D12Backend::TryCancelPendingPacket(OpenXRBackendFrame& packet) { + return m_impl->TryCancelPendingPacket(packet); +} +OpenXRBeginStatus OpenXRD3D12Backend::BeginFrameForPacket(const OpenXRBackendFrame& packet, OpenXRBackendFrame& frame) { + return m_impl->BeginFrameForPacket(packet, frame); +} +OpenXRSubmissionStatus OpenXRD3D12Backend::CopyRenderedEyes(const OpenXRBackendFrame& frame) { + return m_impl->CopyRenderedEyes(frame); +} +OpenXRBeginStatus OpenXRD3D12Backend::KeepAliveCycle() { return m_impl->KeepAliveCycle(); } + bool OpenXRD3D12Backend::Shutdown() { return m_impl->Shutdown(); } bool OpenXRD3D12Backend::IsBound() const { return m_impl->IsBound(); } diff --git a/runtime/src/vr/openxr_integration.cpp b/runtime/src/vr/openxr_integration.cpp index 7786a49..b7176a1 100644 --- a/runtime/src/vr/openxr_integration.cpp +++ b/runtime/src/vr/openxr_integration.cpp @@ -727,6 +727,7 @@ private: VRPresentationMode logged_presentation = VRPresentationMode::Desktop; uint32_t presentation_log_count = 0; bool immersive_submission_logged = false; + int last_pacing_mode = -1; while (!stop_.load(std::memory_order_acquire) && !fatal) { #if defined(__ANDROID__) if (!worker_registered) { @@ -823,19 +824,23 @@ private: const uint32_t interpolation_target = frame_interpolation_fps_.load(std::memory_order_relaxed); SetInterpolationActive(immersive && FrameInterpolationAvailable() && interpolation_target != 0); -#if !defined(_WIN32) - // Standalone (Vulkan) backend: with interpolation off, the eyes are rendered before + // With interpolation off, the eyes are rendered before // the compositor frame that shows them is begun, so that frame never waits for a // game frame. Interpolation keeps the frame-first order below: it renders for the // frame's own predicted display time. - if (!aurora_get_stereo_frame_interpolation()) { + const bool render_first = !aurora_get_stereo_frame_interpolation(); + if (last_pacing_mode != static_cast(render_first)) { + last_pacing_mode = static_cast(render_first); + RT_LOG(RT_TAG_RUNTIME) << "OpenXR " << kGraphicsBackendName << " pacing: " + << (render_first ? "render-first" : "frame-first (VR interpolation)") << std::endl; + } + if (render_first) { if (!RenderFirstCycle(presentation, policy, immersive, consecutive_skips, immersive_submission_logged)) { fatal = true; } continue; } -#endif OpenXRBackendFrame frame{}; const OpenXRBeginStatus begin = backend_->BeginFrame(presentation, frame); if (begin == OpenXRBeginStatus::SessionNotRunning) { @@ -1003,7 +1008,6 @@ private: ShutdownOrRetainGraphicsObjects(); } -#if !defined(_WIN32) // One compositor cycle on the retained layer, with no frame left active. False on a fatal // backend or runtime failure (the error is recorded). bool KeepAlive() { @@ -1023,7 +1027,7 @@ private: return true; } - // Render-first pacing (see OpenXRVulkanBackend::PreparePacket). Returns false on a fatal + // Render-first pacing (see each backend's PreparePacket). Returns false on a fatal // failure; a cycle that ends without a layer returns true and the loop tries again. bool RenderFirstCycle(OpenXRPresentation presentation, const MkwVRPolicySnapshot& policy, bool immersive, uint32_t& consecutive_skips, bool& immersive_submission_logged) { @@ -1089,10 +1093,15 @@ private: } } diagnostics::Measure(diagnostics::Stage::Withdraw, [&] { WithdrawPublishedFrame(); }); - if (stop_.load(std::memory_order_acquire) || canceled_before_encode || + if (stop_.load(std::memory_order_acquire) || submission == OpenXRSubmissionStatus::ShuttingDown) { return true; } + if (canceled_before_encode) { + // Refresh display timing after a canceled packet as well, so the + // next estimate cannot remain stuck in the past during a game stall. + return KeepAlive(); + } if (submission != OpenXRSubmissionStatus::Success) { // Nothing reached the shared buffers (Skipped) or Aurora failed after queuing GPU // work (Failed): same accounting as the frame-first path, on a keep-alive cycle. @@ -1115,7 +1124,7 @@ private: return KeepAlive(); } - // The eyes are in the shared buffers: begin the compositor frame, copy, end. + // The eyes are rendered: begin the compositor frame, complete the backend copy, end. OpenXRBackendFrame frame{}; const OpenXRBeginStatus begin = backend_->BeginFrameForPacket(packet, frame); if (begin == OpenXRBeginStatus::SessionNotRunning) { @@ -1165,7 +1174,6 @@ private: } return true; } -#endif void BuildPublishedFrame(const OpenXRBackendFrame& source, bool immersive, float units_per_meter, uint64_t content_tag) noexcept { diff --git a/runtime/tests/openxr_d3d12_replay_tests.cpp b/runtime/tests/openxr_d3d12_replay_tests.cpp index 5fc1438..a8a0ad3 100644 --- a/runtime/tests/openxr_d3d12_replay_tests.cpp +++ b/runtime/tests/openxr_d3d12_replay_tests.cpp @@ -37,6 +37,8 @@ AuroraD3D12StereoSubmittedCallback callback = nullptr; void* callback_data = nullptr; uint64_t pending_token = 0; bool encoded = false; +bool compositor_open = false; +bool expect_render_first = false; bool should_render = true; bool tracking_valid = true; bool change_space = false; @@ -53,6 +55,7 @@ XrPosef quad_pose{}; void Complete(bool success = true) { Require(pending_token != 0); + if (expect_render_first) Require(!compositor_open); if (success) { for (const auto& target : targets) reinterpret_cast(target.resource)->content = pending_token; @@ -172,16 +175,17 @@ bool OpenXRRuntime::CreateSession(const void*) { ++m_session_run_serial; return true; } -void OpenXRRuntime::DestroySession() { m_session_running = false; } +void OpenXRRuntime::DestroySession() { m_session_running = false; compositor_open = false; } void OpenXRRuntime::ObserveResult(XrResult) noexcept {} OpenXREventStatus OpenXRRuntime::PollEvents() { if (change_space) { ++m_reference_space_change.serial; change_space = false; } - if (restart_session) { ++m_session_run_serial; restart_session = false; } + if (restart_session) { ++m_session_run_serial; m_session_running = true; restart_session = false; } if (stop_session) { m_session_running = false; stop_session = false; } return OpenXREventStatus::Continue; } OpenXRFrameStatus OpenXRRuntime::WaitFrame(OpenXRFrame& frame) { Require(m_frame_phase == FramePhase::Idle); + if (!m_session_running) return OpenXRFrameStatus::SessionNotRunning; frame = {}; frame.serial = m_next_frame_serial++; frame.predicted_display_time = frame.serial * 11'111'111; @@ -194,6 +198,7 @@ OpenXRFrameStatus OpenXRRuntime::WaitFrame(OpenXRFrame& frame) { bool OpenXRRuntime::BeginFrame(const OpenXRFrame& frame) { Require(m_frame_phase == FramePhase::Waited && frame.serial == m_active_frame_serial); m_frame_phase = FramePhase::Begun; + compositor_open = true; return true; } bool OpenXRRuntime::LocateViews(OpenXRFrame& frame) { @@ -206,6 +211,11 @@ bool OpenXRRuntime::LocateViews(OpenXRFrame& frame) { } return true; } +bool OpenXRRuntime::LocateViewsAt(XrTime time, OpenXRFrame& frame) { + Require(!compositor_open); + frame.predicted_display_time = time; + return LocateViews(frame); +} bool OpenXRRuntime::EndFrame(const OpenXRFrame& frame, const XrCompositionLayerBaseHeader* const* layers, uint32_t count) { Require(m_frame_phase == FramePhase::Begun && frame.serial == m_active_frame_serial); @@ -238,6 +248,7 @@ bool OpenXRRuntime::EndFrame(const OpenXRFrame& frame, } } m_frame_phase = FramePhase::Idle; + compositor_open = false; if (fail_end) { fail_end = false; return false; @@ -249,7 +260,142 @@ bool OpenXRRuntime::EndFrameWithoutLayers(const OpenXRFrame& frame) { } } +void TestRenderFirst() { + display_time = 0; + expect_render_first = true; + OpenXRRuntime runtime; + OpenXRD3D12Backend backend; + Require(backend.QueryGraphicsRequirements(runtime) && backend.BindAurora(runtime)); + OpenXRPresentation presentation; + OpenXRBackendFrame packet, frame; + const auto prepare = [&] { + Require(backend.PreparePacket(presentation, packet) == OpenXRBeginStatus::Ready); + Require(!compositor_open && packet.expects_gpu_submission); + }; + const auto submit = [&] { + Complete(); + Require(backend.WaitForSubmission(packet, 0) == OpenXRSubmissionStatus::Success); + Require(backend.BeginFrameForPacket(packet, frame) == OpenXRBeginStatus::Ready); + Require(compositor_open && frame.xr_frame.serial != packet.xr_frame.serial); + Require(backend.CopyRenderedEyes(frame) == OpenXRSubmissionStatus::Success); + Require(backend.FinishFrame(frame, true)); + Require(!compositor_open); + }; + prepare(); + Require(backend.BeginFrameForPacket(packet, frame) == OpenXRBeginStatus::Error); + submit(); + const auto first = displayed_content; + Require(first == packet.xr_frame.serial && layer_count == 1); + prepare(); + const auto before_stall = releases; + encoded = true; + for (int i = 0; i < 300; ++i) { + Require(backend.WaitForSubmission(packet, 0) == OpenXRSubmissionStatus::Timeout); + Require(!backend.TryCancelPendingPacket(packet)); + Require(backend.KeepAliveCycle() == OpenXRBeginStatus::Ready); + Require(!compositor_open && layer_count == 1 && displayed_content == first); + Require(releases == before_stall); + } + submit(); // Must preserve original poses across independently advancing cycles. + Require(displayed_content == packet.xr_frame.serial); + const auto second = displayed_content; + prepare(); + auto stale = packet; + ++stale.xr_frame.serial; + Require(!backend.TryCancelPendingPacket(stale)); + Require(backend.BeginFrameForPacket(stale, frame) == OpenXRBeginStatus::Error); + Require(backend.TryCancelPendingPacket(packet)); + Require(!compositor_open && releases == before_stall + 4); + Require(backend.KeepAliveCycle() == OpenXRBeginStatus::Ready); + Require(displayed_content == second); + + // Original frame-first mode still works after switching interpolation on. + expect_render_first = false; + Require(backend.BeginFrame(presentation, frame) == OpenXRBeginStatus::Ready); + Complete(); + Require(backend.FinishFrame(frame, true)); + expect_render_first = true; + prepare(); + Require(packet.xr_frame.predicted_display_time > display_time); + Require(backend.TryCancelPendingPacket(packet)); + presentation.mode = OpenXRFrameMode::VirtualScreen; + presentation.quad_anchored = true; + presentation.quad_pose.position.z = -4; + prepare(); + Require(targets.size() == 1); + submit(); + Require(layer_type == XR_TYPE_COMPOSITION_LAYER_QUAD && quad_pose.position.z == -4); + + presentation.mode = OpenXRFrameMode::ImmersiveProjection; + prepare(); + should_render = false; // Visibility can change while rendering. + submit(); + Require(layer_count == 0); + Require(backend.PreparePacket(presentation, packet) == OpenXRBeginStatus::Ready); + Require(!packet.expects_gpu_submission); + should_render = true; + Require(backend.KeepAliveCycle() == OpenXRBeginStatus::Ready); + tracking_valid = false; + Require(backend.PreparePacket(presentation, packet) == OpenXRBeginStatus::Ready); + Require(!packet.expects_gpu_submission); + tracking_valid = true; + + for (bool session_change : {false, true}) { + prepare(); + change_space = !session_change; + restart_session = session_change; + runtime.PollEvents(); + submit(); + Require(layer_count == 0); // Do not relabel old images with new space/session serials. + prepare(); + submit(); + Require(layer_count == 1); + } + prepare(); + Complete(); + stop_session = true; + runtime.PollEvents(); + Require(backend.BeginFrameForPacket(packet, frame) == OpenXRBeginStatus::SessionNotRunning); + restart_session = true; + runtime.PollEvents(); + prepare(); + submit(); + Require(layer_count == 1); + + prepare(); + const auto before_failure = releases; + Complete(false); + Require(backend.WaitForSubmission(packet, 0) == OpenXRSubmissionStatus::Failed); + Require(backend.BeginFrameForPacket(packet, frame) == OpenXRBeginStatus::Error); + Require(releases == before_failure); + Require(backend.Shutdown() && live_swapchains == 0); + // Shutdown must also drain packets that never entered a compositor frame, + // including a runtime end failure or session stop during a keep-alive. + for (int scenario = 0; scenario < 3; ++scenario) { + display_time = 0; + OpenXRRuntime next_runtime; + OpenXRD3D12Backend next_backend; + Require(next_backend.QueryGraphicsRequirements(next_runtime)); + Require(next_backend.BindAurora(next_runtime)); + Require(next_backend.PreparePacket(presentation, packet) == OpenXRBeginStatus::Ready); + encoded = true; + if (scenario == 1) { + fail_end = true; + Require(next_backend.KeepAliveCycle() == OpenXRBeginStatus::Error); + } else if (scenario == 2) { + stop_session = true; + next_runtime.PollEvents(); + Require(next_backend.KeepAliveCycle() == OpenXRBeginStatus::SessionNotRunning); + } + Require(!compositor_open); + Require(next_backend.Shutdown() && live_swapchains == 0); + } + expect_render_first = false; + display_time = 0; +} + int main() { + TestRenderFirst(); OpenXRRuntime runtime; OpenXRD3D12Backend backend; Require(backend.QueryGraphicsRequirements(runtime) && backend.BindAurora(runtime));