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

+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));