Implement render-first pacing for D3D12 and Vulkan backends; enhance packet management and diagnostics

This commit is contained in:
iChris4 committed 2026-09-20 17:36:08 +02:00
1 parent 787dbcf7d2
commit fbc9ce7217
5 files changed
+377 -26

No files matched your search

+25 -12
View File
@@ -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
+10
View File
@@ -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
+177 -3
View File
@@ -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(); }
+17 -9
View File
@@ -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<int>(render_first)) {
last_pacing_mode = static_cast<int>(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 {
+148 -2
View File
@@ -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<Image*>(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));