runtime: integrate OpenXR presentation for Mario Kart Wii

This commit is contained in:
iChris4 committed 2026-09-03 04:00:24 +02:00
1 parent d8788de919
commit 2156617cd5
17 files changed
+3052 -42

No files matched your search

+17 -6
View File
@@ -105,10 +105,16 @@ set(MKW_TRANSLATED_COMPILE_JOBS 0 CACHE STRING
Scheduling only - never affects output bytes, so it is deliberately outside the canonical flag fingerprint.")
set(MKW_CPPWINRT_INCLUDE_DIR "" CACHE PATH "Optional self-contained C++/WinRT include directory")
# OpenXR is compiled into supported desktop builds, but remains opt-in at
# runtime through [vr].enabled. Keeping the loader available in every package
# lets one binary fall back to desktop mode when no active runtime is present.
option(MKW_ENABLE_OPENXR "Build OpenXR VR support" ON)
# The functional backend is currently Windows D3D12, so only Windows defaults
# to carrying the loader. Linux keeps the capability-gated Vulkan scaffold
# available to explicit developer builds without making a nonfunctional SDK
# fetch part of every normal build.
set(MKW_ENABLE_OPENXR_DEFAULT OFF)
if(WIN32)
set(MKW_ENABLE_OPENXR_DEFAULT ON)
endif()
option(MKW_ENABLE_OPENXR "Build OpenXR VR support" ${MKW_ENABLE_OPENXR_DEFAULT})
unset(MKW_ENABLE_OPENXR_DEFAULT)
set(MKW_OPENXR_SDK_DIR "" CACHE PATH
"Optional OpenXR-SDK source tree; when empty, use a package or fetch the pinned SDK")
@@ -251,11 +257,16 @@ if(MKW_ENABLE_OPENXR)
add_subdirectory("${MKW_OPENXR_SDK_DIR}"
"${CMAKE_BINARY_DIR}/openxr-sdk" EXCLUDE_FROM_ALL)
else()
FetchContent_Declare(mkw_openxr
# Keep the FetchContent name aligned with the installer's
# BuildWorkspace/Dependencies/openxr directory. The bundled
# build passes FETCHCONTENT_SOURCE_DIR_OPENXR and runs fully
# disconnected, while developer builds may fetch this pin.
FetchContent_Declare(openxr
URL https://github.com/KhronosGroup/OpenXR-SDK/archive/refs/tags/release-1.1.61.tar.gz
URL_HASH SHA256=d5e2773d282642e6e250bd10266f316fc3ff3b8602c83db009a1f434205998e6
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL)
FetchContent_MakeAvailable(mkw_openxr)
FetchContent_MakeAvailable(openxr)
endif()
endif()
if(TARGET OpenXR::openxr_loader)
+30 -1
View File
@@ -49,6 +49,8 @@ struct RuntimeUserConfig {
std::optional<float> vrRenderScale;
std::optional<float> vrWorldUnitsPerMeter;
std::optional<float> vrHudDistanceMeters;
std::optional<bool> vrStopAtDisplayCopy;
std::optional<bool> vrSkipCopyClears;
std::optional<float> audioVolume;
std::optional<float> audioMusicVolume;
std::optional<float> audioSoundEffectsVolume;
@@ -284,7 +286,14 @@ inline void EnsureConfigFile() {
"required = false\n"
"render_scale = 1.0\n"
"world_units_per_meter = 500.0\n"
"hud_distance_meters = 2.0\n\n"
"hud_distance_meters = 2.0\n"
"# EFB replay controls for the per-eye views, changeable live\n"
"# from the F10 menu. stop_at_display_copy ends each eye at the\n"
"# frame's final GXCopyDisp; skip_copy_clears drops the EFB\n"
"# reset a GX copy performs afterwards. Both keep that reset\n"
"# from erasing the eye, and both are safe to turn off.\n"
"stop_at_display_copy = true\n"
"skip_copy_clears = true\n\n"
"[audio]\n"
"volume = 1.0\n"
"music_volume = 1.0\n"
@@ -431,6 +440,8 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
value && *value >= 0.25f && *value <= 10.0f) {
config.vrHudDistanceMeters = *value;
}
config.vrStopAtDisplayCopy = FindConfigValue<bool>(document, "vr", "stop_at_display_copy");
config.vrSkipCopyClears = FindConfigValue<bool>(document, "vr", "skip_copy_clears");
auto readVolume = [&](std::string_view key) -> std::optional<float> {
auto value = FindConfigFloat(document, "audio", key);
@@ -650,6 +661,16 @@ inline bool SetVrEnabled(bool value) {
return WriteSetting("vr", "enabled", value ? "true" : "false");
}
inline bool SetVrStopAtDisplayCopy(bool value) {
Mutable().vrStopAtDisplayCopy = value;
return WriteSetting("vr", "stop_at_display_copy", value ? "true" : "false");
}
inline bool SetVrSkipCopyClears(bool value) {
Mutable().vrSkipCopyClears = value;
return WriteSetting("vr", "skip_copy_clears", value ? "true" : "false");
}
inline bool SetControllerButton(size_t index, std::string value) {
if (index >= kControllerButtonKeys.size()) {
return false;
@@ -821,6 +842,14 @@ inline float VrHudDistanceMeters(float fallback = 2.0f) {
return std::clamp(Get().vrHudDistanceMeters.value_or(fallback), 0.25f, 10.0f);
}
inline bool VrStopAtDisplayCopy(bool fallback = true) {
return Get().vrStopAtDisplayCopy.value_or(fallback);
}
inline bool VrSkipCopyClears(bool fallback = true) {
return Get().vrSkipCopyClears.value_or(fallback);
}
inline std::string GraphicsApi(std::string fallback = "auto") {
return Get().graphicsApi.value_or(std::move(fallback));
}
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <cstdint>
struct CpuContext;
namespace mkw::vr {
// Announces the capabilities supplied by the translated entry observers. It
// is harmless to call repeatedly and does not enable presentation by itself.
void MkwVRInstrumentationInitialize() noexcept;
} // namespace mkw::vr
// Called only from translator-generated PAL RMCP01 function entries. It is a
// read-only observer, not a native replacement: the original translated body
// still runs, and the const context contract is part of translator correctness.
extern "C" void MkwVRObserveTranslatedFunctionEntry(uint32_t address,
const CpuContext* context) noexcept;
+7
View File
@@ -105,6 +105,13 @@ struct MkwVRPolicySnapshot {
MkwVRCameraObservation camera{};
uint32_t available_bindings = MkwVRBindingNone;
bool session_active = false;
// Changes whenever the stable presentation-safety state changes. Ordinary
// per-frame scene/camera publication does not advance it.
uint64_t safety_generation = 1;
// Opaque safety-state tag for the content being sealed. This also includes
// the current presentation mode, so a transient scene/camera mismatch
// cannot accept an immersive packet from an adjacent asynchronous frame.
uint64_t content_tag = 0;
};
// All policy functions are thread-safe. Publishing functions are intended for
+121
View File
@@ -0,0 +1,121 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
#include "vr/openxr_runtime.h"
#include <array>
#include <cstdint>
#include <memory>
#include <string>
namespace mkw::vr {
enum class OpenXRD3D12FrameMode {
ImmersiveProjection,
VirtualScreen,
};
enum class OpenXRD3D12BeginStatus {
Ready,
SessionNotRunning,
ExitRequested,
Error,
};
enum class OpenXRD3D12SubmissionStatus {
Success,
Failed,
Timeout,
ShuttingDown,
};
struct OpenXRD3D12GraphicsRequirements {
uint32_t adapter_luid_low = 0;
int32_t adapter_luid_high = 0;
uint32_t minimum_feature_level = 0;
};
struct OpenXRD3D12Presentation {
OpenXRD3D12FrameMode mode = OpenXRD3D12FrameMode::ImmersiveProjection;
// Used only by VirtualScreen. The quad is head-locked in XR_VIEW_SPACE
// and centered straight ahead at -Z.
float quad_distance_meters = 2.0f;
float quad_width_meters = 2.4f;
};
struct OpenXRD3D12Frame {
OpenXRFrame xr_frame;
OpenXRD3D12Presentation presentation;
std::array<uint32_t, kOpenXREyeCount> render_width{};
std::array<uint32_t, kOpenXREyeCount> render_height{};
bool expects_gpu_submission = false;
};
// Same-device Dawn/OpenXR D3D12 backend.
//
// Startup is deliberately split in two. QueryGraphicsRequirements() runs
// after OpenXRRuntime::Initialize() but before aurora_initialize(), allowing
// its LUID to be placed in AuroraConfig. BindAurora() runs afterwards and
// rejects any device/queue that does not match the queried runtime adapter.
//
// All methods from BeginFrame() through FinishFrame(), plus PollEvents on the
// associated OpenXRRuntime, belong to one XR pacing thread. Aurora's frame
// worker never calls OpenXR: its post-submit callback only publishes a token
// that WaitForSubmission() consumes. This is the synchronization boundary
// required by the asynchronous sealed-frame renderer.
class OpenXRD3D12Backend final {
public:
explicit OpenXRD3D12Backend(OpenXRLogCallback logger = {});
~OpenXRD3D12Backend();
OpenXRD3D12Backend(const OpenXRD3D12Backend&) = delete;
OpenXRD3D12Backend& operator=(const OpenXRD3D12Backend&) = delete;
OpenXRD3D12Backend(OpenXRD3D12Backend&&) = delete;
OpenXRD3D12Backend& operator=(OpenXRD3D12Backend&&) = delete;
bool QueryGraphicsRequirements(OpenXRRuntime& runtime);
bool BindAurora(OpenXRRuntime& runtime);
OpenXRD3D12BeginStatus BeginFrame(const OpenXRD3D12Presentation& presentation,
OpenXRD3D12Frame& frame);
// timeout_ms == UINT32_MAX waits until Aurora publishes this token or
// Shutdown() interrupts the wait. A timeout does not release XR images;
// the caller must first drain/cancel Aurora, then FinishFrame(false).
OpenXRD3D12SubmissionStatus WaitForSubmission(const OpenXRD3D12Frame& frame,
uint32_t timeout_ms = UINT32_MAX);
// Withdraws this token only if Aurora has not encoded it. On success no GPU
// command can reference the acquired images, and FinishFrame(frame, false)
// is required to release them and close the compositor frame.
bool TryCancelPendingFrame(OpenXRD3D12Frame& frame);
// Releases acquired images and calls xrEndFrame. submit_layer must only be
// true after WaitForSubmission returned Success. Immersive frames submit
// XrCompositionLayerProjection; virtual-screen frames submit a head-locked
// XrCompositionLayerQuad using the single mono target.
bool FinishFrame(OpenXRD3D12Frame& frame, bool submit_layer);
// 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
// for the process lifetime instead of destroying possibly live resources.
bool Shutdown();
bool IsBound() const;
const OpenXRD3D12GraphicsRequirements& GraphicsRequirements() const;
int64_t SwapchainFormat() const;
const std::string& LastError() const;
private:
class Impl;
std::unique_ptr<Impl> m_impl;
};
} // namespace mkw::vr
#endif // defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
+39
View File
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <aurora/aurora.h>
#include <string>
namespace mkw::vr {
enum class OpenXRStartupResult {
Disabled,
Prepared,
Unavailable,
};
// Performs the OpenXR instance/system and graphics-requirements work that must
// happen before Aurora selects an adapter. On success this may force the
// backend and populate AuroraConfig's XR interop fields.
OpenXRStartupResult OpenXRPrepareAurora(AuroraConfig& config);
// Completes the graphics binding and starts the asynchronous XR pacing thread.
// Call after aurora_initialize(), while Aurora's frame worker is idle.
bool OpenXRStartAfterAurora(AuroraBackend active_backend);
// Stops publishing stereo work, drains the pacing thread, and destroys the XR
// session before aurora_shutdown(). Safe to call after partial initialization.
void OpenXRShutdownBeforeAurora() noexcept;
// Services an XR-owned teardown request at the producer's safe frame boundary:
// after aurora_begin_frame() has granted the worker's prepare phase and before
// aurora_end_frame_tagged() seals the current frame. No-op unless the pacing
// thread has fallen back to desktop rendering.
void OpenXRServiceProducerFrameBoundary() noexcept;
bool OpenXRIsRunning() noexcept;
std::string OpenXRLastError();
} // namespace mkw::vr
+17 -1
View File
@@ -142,6 +142,11 @@ public:
OpenXREventStatus PollEvents();
bool RequestExitSession();
// Graphics backends call OpenXR entry points directly for swapchain work.
// Feed their results back here so positive loss-pending and negative
// session/instance-loss results update the central lifecycle state.
void ObserveResult(XrResult result) noexcept;
// Explicit frame protocol. A successful WaitFrame must be followed by
// BeginFrame and EndFrame using the same token. LocateViews is optional when
// should_render is false; it is otherwise normally called after BeginFrame.
@@ -171,7 +176,9 @@ public:
return m_session_state == XR_SESSION_STATE_VISIBLE ||
m_session_state == XR_SESSION_STATE_FOCUSED;
}
bool ShouldExit() const { return m_exit_requested || m_instance_loss_pending; }
bool ShouldExit() const {
return m_exit_requested || m_session_loss_pending || m_instance_loss_pending;
}
XrInstance Instance() const { return m_instance; }
XrSystemId SystemId() const { return m_system_id; }
@@ -179,6 +186,7 @@ public:
XrSpace AppSpace() const { return m_app_space; }
XrSpace ViewSpace() const { return m_view_space; }
XrSessionState SessionState() const { return m_session_state; }
uint64_t SessionRunSerial() const { return m_session_run_serial; }
XrReferenceSpaceType AppSpaceType() const { return m_app_space_type; }
XrEnvironmentBlendMode EnvironmentBlendMode() const { return m_blend_mode; }
@@ -194,6 +202,10 @@ public:
const OpenXRReferenceSpaceChange& LastReferenceSpaceChange() const {
return m_reference_space_change;
}
// Consumes every queued application-space change whose effective time is
// no later than display_time. Multiple future recenter events are retained
// independently rather than overwriting one another.
bool ConsumeAppSpaceChangesThrough(XrTime display_time);
const OpenXRError& LastError() const { return m_last_error; }
private:
@@ -212,6 +224,7 @@ private:
bool EnumerateSwapchainFormats();
bool HandleSessionStateChanged(const XrEventDataSessionStateChanged& event);
bool IsFrameTokenCurrent(const OpenXRFrame& frame, FramePhase expected) const;
void ResetFrameState();
void DestroyReferenceSpaces();
void ResetSessionState();
void ResetInstanceState();
@@ -237,10 +250,12 @@ private:
XrEnvironmentBlendMode m_blend_mode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
bool m_session_running = false;
bool m_exit_requested = false;
bool m_session_loss_pending = false;
bool m_instance_loss_pending = false;
bool m_shutting_down_session = false;
FramePhase m_frame_phase = FramePhase::Idle;
uint64_t m_session_run_serial = 0;
uint64_t m_next_frame_serial = 1;
uint64_t m_active_frame_serial = 0;
XrTime m_active_frame_display_time = 0;
@@ -255,6 +270,7 @@ private:
std::vector<XrEnvironmentBlendMode> m_supported_blend_modes;
std::vector<int64_t> m_swapchain_formats;
OpenXRReferenceSpaceChange m_reference_space_change;
std::vector<OpenXRReferenceSpaceChange> m_pending_app_space_changes;
};
} // namespace mkw::vr
+186
View File
@@ -0,0 +1,186 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#if defined(MKW_ENABLE_OPENXR)
#include "vr/openxr_runtime.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
namespace mkw::vr {
// Keep Vulkan implementation types out of the runtime/Aurora boundary. Vulkan
// dispatchable and non-dispatchable handles both fit in 64 bits on every target
// supported by this project. The backend converts these values to the Vulkan
// header's actual handle types only in its implementation file.
using OpenXRVulkanHandle = uint64_t;
inline constexpr uint32_t kOpenXRVulkanNativeContextVersion = 1;
enum OpenXRVulkanNativeContextFlagBits : uint32_t {
// The handles are the exact objects used by Aurora's Dawn device. A second
// Vulkan device is not a valid substitute: it cannot present Dawn's work to
// an OpenXR swapchain without an explicit external-memory bridge.
OpenXRVulkanNativeContextDawnOwnedBit = 1u << 0,
// The provider has queried the queue from device/queue_family_index/
// queue_index and verified that it equals queue. Dawn's package API does not
// currently expose enough information for this backend to do that itself.
OpenXRVulkanNativeContextQueueVerifiedBit = 1u << 1,
};
using OpenXRVulkanQueueLockCallback = bool (*)(void* userdata);
using OpenXRVulkanQueueUnlockCallback = void (*)(void* userdata);
// ABI-stable context a Dawn-native shim must provide. All handles are borrowed
// and must outlive OpenXRVulkanBackend. The paired callbacks externally
// synchronize Vulkan queue access with Dawn; they are mandatory because a
// Vulkan queue is not internally synchronized and XR may use the graphics queue
// during image hand-off.
struct OpenXRVulkanNativeContext {
uint32_t version = kOpenXRVulkanNativeContextVersion;
uint32_t struct_size = sizeof(OpenXRVulkanNativeContext);
uint32_t flags = 0;
OpenXRVulkanHandle instance = 0;
OpenXRVulkanHandle physical_device = 0;
OpenXRVulkanHandle device = 0;
OpenXRVulkanHandle queue = 0;
uint32_t queue_family_index = 0;
uint32_t queue_index = 0;
// A VK_MAKE_API_VERSION encoded value for the Vulkan instance/device.
uint32_t api_version = 0;
OpenXRVulkanQueueLockCallback lock_queue = nullptr;
OpenXRVulkanQueueUnlockCallback unlock_queue = nullptr;
void* queue_userdata = nullptr;
};
enum class OpenXRVulkanCapability {
Available,
VulkanHeadersUnavailable,
DawnNativeHandlesUnavailable,
InvalidNativeContext,
MissingEnable2Extension,
RuntimeRejectedGraphicsDevice,
RuntimeApiVersionMismatch,
};
struct OpenXRVulkanCapabilityInfo {
OpenXRVulkanCapability capability = OpenXRVulkanCapability::Available;
std::string reason;
explicit operator bool() const {
return capability == OpenXRVulkanCapability::Available;
}
};
struct OpenXRVulkanBackendConfig {
// Rendering directly into an XR image needs COLOR_ATTACHMENT. Aurora's
// intended zero-copy bridge renders to an imported/application-owned image
// and performs a GPU copy, which additionally requires TRANSFER_DST.
bool require_transfer_destination = true;
XrDuration image_wait_timeout = XR_INFINITE_DURATION;
XrCompositionLayerFlags projection_layer_flags = 0;
};
struct OpenXRVulkanEyeImage {
uint32_t eye = 0;
uint32_t image_index = 0;
uint32_t width = 0;
uint32_t height = 0;
int64_t format = 0;
OpenXRVulkanHandle image = 0;
};
// Owns the Vulkan-bound session and its per-eye OpenXR swapchains. It does not
// record Vulkan commands: Aurora's backend bridge is responsible for rendering
// or copying into the borrowed VkImage on the supplied Dawn queue, submitting
// that GPU work, then calling ReleaseEyeImage(). No CPU/readback fallback exists.
//
// All methods must be called on the XR owner thread. Shutdown() must run before
// the native Vulkan device is destroyed.
class OpenXRVulkanBackend final {
public:
explicit OpenXRVulkanBackend(OpenXRLogCallback logger = {});
~OpenXRVulkanBackend();
OpenXRVulkanBackend(const OpenXRVulkanBackend&) = delete;
OpenXRVulkanBackend& operator=(const OpenXRVulkanBackend&) = delete;
OpenXRVulkanBackend(OpenXRVulkanBackend&&) = delete;
OpenXRVulkanBackend& operator=(OpenXRVulkanBackend&&) = delete;
// Reports whether this build's Aurora/Dawn provider has the native Vulkan
// handle shim required by the backend. The pinned prebuilt Dawn package does
// not expose VkPhysicalDevice, VkDevice, VkQueue, or the queue-family index,
// so this intentionally reports DawnNativeHandlesUnavailable until such a
// shim is compiled in.
static OpenXRVulkanCapabilityInfo DawnInteropCapability();
// runtime must already be initialized with XR_KHR_vulkan_enable2 in its
// required extension list and must not yet own a session.
bool Initialize(OpenXRRuntime& runtime,
const OpenXRVulkanNativeContext& native_context,
const OpenXRVulkanBackendConfig& config = {});
void Shutdown();
bool AcquireEyeImage(uint32_t eye, OpenXRVulkanEyeImage& image);
bool ReleaseEyeImage(uint32_t eye);
// Ends a begun OpenXR frame with a stereo projection layer. The pose/FOV
// come from the same OpenXRFrame token used for rendering. When
// should_render is false or views are invalid, the frame is ended without a
// layer as required by the OpenXR frame protocol.
bool SubmitProjection(const OpenXRFrame& frame);
bool IsInitialized() const { return m_runtime != nullptr; }
int64_t SwapchainFormat() const { return m_swapchain_format; }
const std::array<OpenXRViewConfiguration, kOpenXREyeCount>&
ViewConfiguration() const;
const OpenXRError& LastError() const { return m_last_error; }
private:
struct EyeSwapchain {
XrSwapchain handle = XR_NULL_HANDLE;
uint32_t width = 0;
uint32_t height = 0;
std::vector<OpenXRVulkanHandle> images;
uint32_t acquired_index = 0;
bool acquired = false;
bool waited = false;
};
bool ValidateNativeContext(const OpenXRVulkanNativeContext& native_context);
bool ValidateRuntimeDevice();
bool CreateSwapchains();
bool CreateEyeSwapchain(uint32_t eye);
bool SelectSwapchainFormat();
void DestroySwapchains();
bool LockQueue();
void UnlockQueue();
bool Check(XrResult result, std::string_view operation);
bool Fail(XrResult result, std::string_view operation, std::string_view detail);
void Log(OpenXRLogLevel level, std::string_view message) const noexcept;
std::string ResultString(XrResult result) const;
OpenXRRuntime* m_runtime = nullptr;
OpenXRLogCallback m_logger;
OpenXRError m_last_error;
OpenXRVulkanNativeContext m_native_context{};
OpenXRVulkanBackendConfig m_config{};
std::array<EyeSwapchain, kOpenXREyeCount> m_eye_swapchains{};
int64_t m_swapchain_format = 0;
bool m_owns_session = false;
};
} // namespace mkw::vr
#endif // defined(MKW_ENABLE_OPENXR)
+13 -1
View File
@@ -7,6 +7,8 @@
#include "settings_overlay.h"
#include "fiber_manager.h"
#include "runtime_log.h"
#include "vr/mkw_vr_policy.h"
#include "vr/openxr_integration.h"
#include <dolphin/vi.h>
@@ -333,6 +335,10 @@ void AdvanceRetrace(CpuContext* ctx, Clock::time_point retraceStamp, bool servic
g_auroraFrameActive.store(true, std::memory_order_release);
}
}
// XR runtime loss and encoded-work stalls request producer-owned
// teardown. Service it on every retrace, including startup, pause, and
// minimized-window paths that may never reach a successful present.
mkw::vr::OpenXRServiceProducerFrameBoundary();
}
if (ctx) {
@@ -579,7 +585,13 @@ void VI_HLE_PresentFrame(bool presentedXfb, bool paceToRetrace) {
aurora_set_present_schedule(0, 0);
}
aurora_end_frame();
mkw::vr::OpenXRServiceProducerFrameBoundary();
// Latch the current policy safety state into this exact Aurora job. The
// asynchronous worker may ask for an XR packet after the guest has already
// begun the next frame, so immersive replay is accepted only when both
// tags match.
const uint64_t vrContentTag = mkw::vr::MkwVRPolicyGetSnapshot().content_tag;
aurora_end_frame_tagged(vrContentTag);
if (paceThisFrame) {
PaceToRetraceBoundary(paceDeadline);
std::lock_guard<std::mutex> lock(g_viMutex);
+25
View File
@@ -55,6 +55,7 @@
#include "runtime_log.h"
#include "runtime_product.h"
#include "recomp_mod_loader.h"
#include "vr/openxr_integration.h"
#include <aurora/aurora.h>
#include <aurora/gfx.h>
#include <dolphin/gx/GXAurora.h>
@@ -1362,6 +1363,16 @@ int RuntimeMain(int argc, char** argv) {
break;
}
}
const mkw::vr::OpenXRStartupResult openxrStartup =
mkw::vr::OpenXRPrepareAurora(auroraConfig);
if (openxrStartup == mkw::vr::OpenXRStartupResult::Unavailable) {
const std::string error = mkw::vr::OpenXRLastError();
if (RuntimeConfigFile::VrRequired(false)) {
throw std::runtime_error("OpenXR is required but unavailable: " + error);
}
RT_LOG(RT_TAG_RUNTIME) << "OpenXR unavailable; continuing with desktop rendering: "
<< error << std::endl;
}
const AuroraBackend requestedBackend = auroraConfig.desiredBackend;
const AuroraInfo auroraInfo = aurora_initialize(0, nullptr, &auroraConfig);
@@ -1391,6 +1402,17 @@ int RuntimeMain(int argc, char** argv) {
<< std::endl;
g_auroraInitialized.store(true, std::memory_order_release);
if (openxrStartup == mkw::vr::OpenXRStartupResult::Prepared &&
!mkw::vr::OpenXRStartAfterAurora(auroraInfo.backend)) {
const std::string error = mkw::vr::OpenXRLastError();
if (RuntimeConfigFile::VrRequired(false)) {
throw std::runtime_error("OpenXR graphics binding failed: " + error);
}
RT_LOG(RT_TAG_RUNTIME)
<< "OpenXR graphics binding failed; continuing with the desktop mirror: "
<< error << std::endl;
}
auto entry = ResolveEntry();
InitializePersistentCpuContext();
auto& cpu = GetPersistentCpuContext();
@@ -1417,6 +1439,7 @@ int RuntimeMain(int argc, char** argv) {
// Shutdown fiber system
Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true);
mkw::vr::OpenXRShutdownBeforeAurora();
aurora_shutdown();
SetRuntimeExitCodeImpl(0);
ShutdownProcessTranscript();
@@ -1434,6 +1457,7 @@ int RuntimeMain(int argc, char** argv) {
SetRuntimeExitCodeImpl(1);
Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true);
mkw::vr::OpenXRShutdownBeforeAurora();
aurora_shutdown();
ShutdownProcessTranscript();
return 1;
@@ -1445,6 +1469,7 @@ int RuntimeMain(int argc, char** argv) {
SetRuntimeExitCodeImpl(1);
Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true);
mkw::vr::OpenXRShutdownBeforeAurora();
aurora_shutdown();
ShutdownProcessTranscript();
return 1;
+33
View File
@@ -99,6 +99,8 @@ bool g_skipUnreadyPipelines = RuntimeConfigFile::SkipUnreadyPipelines(true);
bool g_disableCopyFilter = RuntimeConfigFile::DisableCopyFilter(true);
bool g_showFps = RuntimeConfigFile::ShowFps(true);
bool g_vrEnabled = RuntimeConfigFile::VrEnabled(false);
bool g_vrStopAtDisplayCopy = RuntimeConfigFile::VrStopAtDisplayCopy(true);
bool g_vrSkipCopyClears = RuntimeConfigFile::VrSkipCopyClears(true);
uint32_t g_disabledPostProcessingPaths = RuntimeConfigFile::DisabledPostProcessingPaths(0);
std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] {
std::array<int32_t, PAD_MAX_CONTROLLERS> indices{};
@@ -650,6 +652,35 @@ void DrawGraphicsSettings() {
RuntimeConfigFile::SetVrEnabled(g_vrEnabled);
}
ImGui::TextDisabled("OpenXR mode changes take effect after restarting the game.");
// Unlike the toggle above, these two apply to the very next frame, so they
// can be compared against each other from inside a running race.
ImGui::Separator();
ImGui::Text("VR eye replay (EFB)");
if (ImGui::Checkbox("Stop eye at display copy", &g_vrStopAtDisplayCopy)) {
aurora_set_stereo_stop_at_display_copy(g_vrStopAtDisplayCopy);
RuntimeConfigFile::SetVrStopAtDisplayCopy(g_vrStopAtDisplayCopy);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Ends each eye at the frame's final GXCopyDisp, so an eye holds exactly the "
"image the game presented. Turn off to replay the whole pass list.");
}
if (ImGui::Checkbox("Skip EFB copy clears", &g_vrSkipCopyClears)) {
aurora_set_stereo_skip_copy_clears(g_vrSkipCopyClears);
RuntimeConfigFile::SetVrSkipCopyClears(g_vrSkipCopyClears);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Drops the EFB reset a GX copy performs after copying. That reset prepares the "
"Wii's reused EFB for the next frame; an eye attachment is built fresh, so "
"replaying it only erases the eye.");
}
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::TextDisabled(
"Both apply on the next frame. Turning either off restores the raw replay and is "
"expected to black out the eyes.");
ImGui::PopTextWrapPos();
}
void DrawFpsOverlay() {
@@ -867,6 +898,8 @@ void InitializeRuntimeSettings() noexcept {
aurora_set_display_mode(static_cast<AuroraDisplayMode>(g_displayMode));
g_displayMode = static_cast<int>(aurora_get_display_mode());
aurora_set_disable_copy_filter(g_disableCopyFilter);
aurora_set_stereo_stop_at_display_copy(g_vrStopAtDisplayCopy);
aurora_set_stereo_skip_copy_clears(g_vrSkipCopyClears);
aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines);
g_strapInputAccepted.store(false, std::memory_order_relaxed);
g_startupDismissFrame.store(UINT64_MAX, std::memory_order_relaxed);
+187
View File
@@ -0,0 +1,187 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "vr/mkw_vr_instrumentation.h"
#include "memory.h"
#include "ppc_runtime.h"
#include "runtime_log.h"
#include "vr/mkw_vr_policy.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <mutex>
extern "C" int g_gxFrameCount;
namespace mkw::vr {
namespace {
constexpr uint32_t kRaceSceneOnEnter = 0x80553C50u;
constexpr uint32_t kRaceSceneOnExit = 0x805549B0u;
constexpr uint32_t kRaceCameraUpdate = 0x805A21D0u;
constexpr uint32_t kScnMgrRaceDraw = 0x805B1CD8u;
// PAL RMCP01 RaceScene::GetScreenCount (0x80554F68) reads the active
// RaceScene pointer from this SDA-backed slot, then returns byte 0x25. Keep
// this synchronized with projects/mkwii/MAP.txt and the generated function.
constexpr uint32_t kRaceSceneInstanceAddress = 0x809BD728u;
constexpr uint32_t kRaceSceneScreenCountOffset = 0x25u;
struct InstrumentationState {
uint64_t guest_frame = 0;
std::array<uint32_t, 4> cameras{};
uint32_t camera_count = 0;
uint32_t last_reported_camera_count = UINT32_MAX;
uint32_t last_reported_screen_count = UINT32_MAX;
};
std::mutex g_instrumentation_mutex;
InstrumentationState g_instrumentation;
uint64_t GuestFrame() noexcept {
return static_cast<uint64_t>(static_cast<uint32_t>(g_gxFrameCount));
}
void ResetCamerasForFrameLocked(uint64_t frame) noexcept {
if (g_instrumentation.guest_frame == frame) {
return;
}
g_instrumentation.guest_frame = frame;
g_instrumentation.cameras = {};
g_instrumentation.camera_count = 0;
}
uint32_t ObserveCamera(uint64_t frame, uint32_t camera) noexcept {
std::lock_guard lock(g_instrumentation_mutex);
ResetCamerasForFrameLocked(frame);
const auto first = g_instrumentation.cameras.begin();
const auto last = first + g_instrumentation.camera_count;
if (camera != 0 && std::find(first, last, camera) == last &&
g_instrumentation.camera_count < g_instrumentation.cameras.size()) {
g_instrumentation.cameras[g_instrumentation.camera_count++] = camera;
}
return g_instrumentation.camera_count;
}
uint32_t CameraCount(uint64_t frame) noexcept {
std::lock_guard lock(g_instrumentation_mutex);
ResetCamerasForFrameLocked(frame);
return g_instrumentation.camera_count;
}
uint32_t RaceScreenCount() noexcept {
uint32_t race_scene = 0;
if (!Memory::TryRead32(kRaceSceneInstanceAddress, race_scene) || race_scene == 0 ||
!Memory::Contains(race_scene + kRaceSceneScreenCountOffset)) {
return 0;
}
try {
const uint32_t screen_count =
Memory::Read8(race_scene + kRaceSceneScreenCountOffset);
return screen_count <= 4 ? screen_count : 0;
} catch (const Memory::AccessViolation&) {
return 0;
}
}
void LogRaceEvidenceIfChanged(uint64_t frame, uint32_t screen_count,
uint32_t camera_count) noexcept {
bool changed = false;
{
std::lock_guard lock(g_instrumentation_mutex);
changed = g_instrumentation.last_reported_screen_count != screen_count ||
g_instrumentation.last_reported_camera_count != camera_count;
g_instrumentation.last_reported_screen_count = screen_count;
g_instrumentation.last_reported_camera_count = camera_count;
}
if (changed) {
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] race evidence: frame=" << frame
<< ", screens=" << screen_count
<< ", updated cameras=" << camera_count << std::endl;
}
}
void PublishRaceScene(uint64_t frame, uint32_t local_players) noexcept {
MkwVRSceneObservation scene{};
scene.mode = VRSceneMode::Race;
scene.local_player_count = local_players;
scene.guest_frame_index = frame;
MkwVRPolicyPublishScene(scene);
}
void PublishObservedCamera(uint64_t frame, uint32_t address) noexcept {
// Aurora's stereo replay composes the OpenXR eye transform with the GX
// matrices recorded by the original game. The policy only needs evidence
// that the RaceCamera path ran; it never consumes this identity sample to
// replace the game's view matrix.
MkwVRCameraObservation camera{};
camera.view_from_world = {
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
};
camera.guest_camera_address = address;
camera.guest_frame_index = frame;
camera.valid = address != 0;
MkwVRPolicyPublishRaceCamera(camera);
}
} // namespace
void MkwVRInstrumentationInitialize() noexcept {
MkwVRPolicySetAvailableBindings(
MkwVRBindingSceneState | MkwVRBindingRaceCamera |
MkwVRBindingDrawClassification);
}
} // namespace mkw::vr
extern "C" void MkwVRObserveTranslatedFunctionEntry(uint32_t address,
const CpuContext* context) noexcept {
using namespace mkw::vr;
const uint64_t frame = GuestFrame();
switch (address) {
case kRaceSceneOnEnter: {
{
std::lock_guard lock(g_instrumentation_mutex);
g_instrumentation = {};
g_instrumentation.guest_frame = frame;
}
PublishRaceScene(frame, 0);
MkwVRPolicyInvalidateRaceCamera();
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] entered RaceScene at frame " << frame
<< std::endl;
break;
}
case kRaceCameraUpdate: {
const uint32_t camera_address = context != nullptr ? context->gpr[3] : 0;
ObserveCamera(frame, camera_address);
PublishObservedCamera(frame, camera_address);
break;
}
case kScnMgrRaceDraw: {
// RaceCamera objects are not a player-count source: the game may update
// additional cameras for transitions and effects. Use the same exact
// screen count as RaceScene::GetScreenCount, while retaining the camera
// hook as independent evidence that a usable race camera is current.
const uint32_t camera_count = CameraCount(frame);
const uint32_t screen_count = RaceScreenCount();
LogRaceEvidenceIfChanged(frame, screen_count, camera_count);
PublishRaceScene(frame, screen_count);
break;
}
case kRaceSceneOnExit: {
MkwVRSceneObservation scene{};
scene.mode = VRSceneMode::Other;
scene.guest_frame_index = frame;
MkwVRPolicyPublishScene(scene);
MkwVRPolicyInvalidateRaceCamera();
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] exited RaceScene at frame " << frame
<< std::endl;
break;
}
default:
break;
}
}
+80 -15
View File
@@ -17,6 +17,7 @@ struct PolicyState {
MkwVRCameraObservation camera{};
uint32_t available_bindings = MkwVRBindingNone;
bool session_active = false;
uint64_t safety_generation = 1;
};
std::mutex g_policy_mutex;
@@ -61,13 +62,45 @@ bool IsFiniteCamera(const MkwVRCameraObservation& camera) noexcept {
[](const float& value) { return IsFiniteFloat(&value); });
}
bool ObservationsAreCoherent(const MkwVRSceneObservation& scene,
const MkwVRCameraObservation& camera) noexcept {
// RaceCamera::Update for guest frame N+1 can run before ScnMgrRace::Draw
// publishes the scene observation for that frame. The OpenXR pacing thread
// is independent and can legitimately sample that short interval. Accept
// adjacent frames from the same live RaceScene; larger gaps still fail
// closed, and scene transitions explicitly invalidate the camera.
const uint64_t newer = std::max(scene.guest_frame_index, camera.guest_frame_index);
const uint64_t older = std::min(scene.guest_frame_index, camera.guest_frame_index);
return newer - older <= 1;
}
VRPresentationMode SelectPresentation(const PolicyState& state) noexcept {
if (!state.config.enabled || !state.session_active) {
return VRPresentationMode::Desktop;
}
// A virtual screen is the fail-safe for menus, replays, split-screen, and
// any incomplete instrumentation. It preserves the unmodified render path.
// A virtual screen is the fail-safe for menus, split-screen, and any
// incomplete instrumentation. It preserves the unmodified render path.
if ((state.available_bindings & kMkwVRRequiredImmersiveBindings) !=
kMkwVRRequiredImmersiveBindings ||
!state.config.immersive_races || state.scene.mode != VRSceneMode::Race ||
state.scene.local_player_count != 1 || !IsFiniteCamera(state.camera) ||
!ObservationsAreCoherent(state.scene, state.camera)) {
return VRPresentationMode::VirtualScreen;
}
return VRPresentationMode::ImmersiveRace;
}
VRPresentationMode SelectStablePresentation(const PolicyState& state) noexcept {
if (!state.config.enabled || !state.session_active) {
return VRPresentationMode::Desktop;
}
// Deliberately omit the per-frame scene/camera index comparison here.
// Those observations are published by separate translated callbacks, so
// their temporary mismatch is represented in content_tag's mode bits
// without advancing the generation twice on every healthy race frame.
if ((state.available_bindings & kMkwVRRequiredImmersiveBindings) !=
kMkwVRRequiredImmersiveBindings ||
!state.config.immersive_races || state.scene.mode != VRSceneMode::Race ||
@@ -78,6 +111,32 @@ VRPresentationMode SelectPresentation(const PolicyState& state) noexcept {
return VRPresentationMode::ImmersiveRace;
}
void AdvanceSafetyGeneration(PolicyState& state) noexcept {
// Two low bits are reserved for VRPresentationMode in MakeContentTag().
// Keep UINT64_MAX reserved as Aurora's explicit unknown-tag sentinel.
constexpr uint64_t kMaxSafetyGeneration = (UINT64_MAX >> 2) - 1;
if (state.safety_generation >= kMaxSafetyGeneration) {
state.safety_generation = 1;
} else {
++state.safety_generation;
}
}
template <typename Mutation>
void ApplyPolicyMutation(Mutation&& mutation) noexcept {
const VRPresentationMode previous = SelectStablePresentation(g_policy);
mutation();
if (SelectStablePresentation(g_policy) != previous) {
AdvanceSafetyGeneration(g_policy);
}
}
uint64_t MakeContentTag(const PolicyState& state, VRPresentationMode presentation) noexcept {
static_assert(static_cast<uint64_t>(VRPresentationMode::ImmersiveRace) < 4,
"VRPresentationMode must fit in the content tag's reserved bits");
return (state.safety_generation << 2) | static_cast<uint64_t>(presentation);
}
constexpr MkwVRHookPoint kHookPoints[] = {
{0x80562B34u, "ScnMgr::UpdateCameras", MkwVRHookCapability::SceneState,
"Observe the active scene camera update boundary."},
@@ -130,39 +189,43 @@ void MkwVRPolicyReset() noexcept {
void MkwVRPolicyConfigure(const MkwVRPolicyConfig& config) noexcept {
std::lock_guard<std::mutex> lock(g_policy_mutex);
g_policy.config = SanitizeConfig(config);
ApplyPolicyMutation([&] { g_policy.config = SanitizeConfig(config); });
}
void MkwVRPolicySetSessionActive(bool active) noexcept {
std::lock_guard<std::mutex> lock(g_policy_mutex);
g_policy.session_active = active;
ApplyPolicyMutation([&] { g_policy.session_active = active; });
}
void MkwVRPolicySetAvailableBindings(uint32_t bindings) noexcept {
std::lock_guard<std::mutex> lock(g_policy_mutex);
g_policy.available_bindings = bindings;
ApplyPolicyMutation([&] { g_policy.available_bindings = bindings; });
}
void MkwVRPolicyPublishScene(const MkwVRSceneObservation& scene) noexcept {
std::lock_guard<std::mutex> lock(g_policy_mutex);
if (scene.mode != VRSceneMode::Race || g_policy.scene.mode != VRSceneMode::Race) {
// Never carry a camera sample across a menu/replay-to-race transition.
// A fresh RaceCamera observation must arrive before immersive mode can
// become active again.
g_policy.camera.valid = false;
}
g_policy.scene = scene;
ApplyPolicyMutation([&] {
if (scene.mode != VRSceneMode::Race || g_policy.scene.mode != VRSceneMode::Race) {
// Never carry a camera sample across a menu/replay-to-race transition.
// A fresh RaceCamera observation must arrive before immersive mode can
// become active again.
g_policy.camera.valid = false;
}
g_policy.scene = scene;
});
}
void MkwVRPolicyPublishRaceCamera(const MkwVRCameraObservation& camera) noexcept {
std::lock_guard<std::mutex> lock(g_policy_mutex);
g_policy.camera = camera;
g_policy.camera.valid = IsFiniteCamera(camera);
ApplyPolicyMutation([&] {
g_policy.camera = camera;
g_policy.camera.valid = IsFiniteCamera(camera);
});
}
void MkwVRPolicyInvalidateRaceCamera() noexcept {
std::lock_guard<std::mutex> lock(g_policy_mutex);
g_policy.camera.valid = false;
ApplyPolicyMutation([&] { g_policy.camera.valid = false; });
}
MkwVRPolicySnapshot MkwVRPolicyGetSnapshot() noexcept {
@@ -174,6 +237,8 @@ MkwVRPolicySnapshot MkwVRPolicyGetSnapshot() noexcept {
snapshot.camera = g_policy.camera;
snapshot.available_bindings = g_policy.available_bindings;
snapshot.session_active = g_policy.session_active;
snapshot.safety_generation = g_policy.safety_generation;
snapshot.content_tag = MakeContentTag(g_policy, snapshot.presentation);
return snapshot;
}
+813
View File
@@ -0,0 +1,813 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
// OpenXR's D3D12 structures are selected when openxr_platform.h is parsed.
#define XR_USE_GRAPHICS_API_D3D12
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "vr/openxr_d3d12.h"
#include <aurora/d3d12_interop.h>
#include <d3d12.h>
#include <dxgi1_4.h>
#include <openxr/openxr_platform.h>
#include <algorithm>
#include <array>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <limits>
#include <mutex>
#include <sstream>
#include <utility>
#include <vector>
namespace mkw::vr {
namespace {
bool SameDxgiCopyFamily(DXGI_FORMAT left, DXGI_FORMAT right) noexcept {
const auto family = [](DXGI_FORMAT format) noexcept {
switch (format) {
case DXGI_FORMAT_R8G8B8A8_TYPELESS:
case DXGI_FORMAT_R8G8B8A8_UNORM:
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
return 1;
case DXGI_FORMAT_B8G8R8A8_TYPELESS:
case DXGI_FORMAT_B8G8R8A8_UNORM:
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
return 2;
case DXGI_FORMAT_R16G16B16A16_TYPELESS:
case DXGI_FORMAT_R16G16B16A16_FLOAT:
return 3;
default:
return 0;
}
};
const int left_family = family(left);
return left_family != 0 && left_family == family(right);
}
DXGI_FORMAT SrgbSibling(DXGI_FORMAT format) noexcept {
switch (format) {
case DXGI_FORMAT_R8G8B8A8_TYPELESS:
case DXGI_FORMAT_R8G8B8A8_UNORM:
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
return DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
case DXGI_FORMAT_B8G8R8A8_TYPELESS:
case DXGI_FORMAT_B8G8R8A8_UNORM:
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
return DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
default:
return DXGI_FORMAT_UNKNOWN;
}
}
bool IsSrgbFormat(DXGI_FORMAT format) noexcept {
return format == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB ||
format == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
}
const char* BeginStatusOperation(OpenXRFrameStatus status) noexcept {
switch (status) {
case OpenXRFrameStatus::Ready:
return "ready";
case OpenXRFrameStatus::SessionNotRunning:
return "session is not running";
case OpenXRFrameStatus::ExitRequested:
return "runtime requested exit";
case OpenXRFrameStatus::Error:
return "xrWaitFrame failed";
}
return "unknown frame status";
}
} // namespace
class OpenXRD3D12Backend::Impl final {
public:
explicit Impl(OpenXRLogCallback logger) : logger_(std::move(logger)) {}
~Impl() { Shutdown(); }
struct EyeSwapchain {
XrSwapchain handle = XR_NULL_HANDLE;
uint32_t width = 0;
uint32_t height = 0;
std::vector<XrSwapchainImageD3D12KHR> images;
uint32_t acquired_index = 0;
bool acquired = false;
bool waited = false;
bool release_forbidden = false;
};
bool QueryGraphicsRequirements(OpenXRRuntime& runtime) {
ClearError();
if (!runtime.IsInitialized() || runtime.HasSession()) {
return Fail("OpenXR must own an instance, but no session, before querying D3D12 requirements");
}
if (requirements_queried_ && runtime_ != &runtime) {
return Fail("D3D12 graphics requirements were already queried from another OpenXR instance");
}
PFN_xrGetD3D12GraphicsRequirementsKHR get_requirements = nullptr;
if (!runtime.LoadFunction("xrGetD3D12GraphicsRequirementsKHR", &get_requirements) ||
get_requirements == nullptr) {
return Fail("OpenXR runtime did not expose xrGetD3D12GraphicsRequirementsKHR");
}
XrGraphicsRequirementsD3D12KHR requirements{XR_TYPE_GRAPHICS_REQUIREMENTS_D3D12_KHR};
const XrResult result = get_requirements(runtime.Instance(), runtime.SystemId(), &requirements);
runtime.ObserveResult(result);
if (XR_FAILED(result)) {
std::ostringstream message;
message << "xrGetD3D12GraphicsRequirementsKHR failed (" << result << ')';
return Fail(message.str());
}
runtime_ = &runtime;
{
std::lock_guard lock(submission_mutex_);
shutting_down_ = false;
submission_unsafe_ = false;
}
requirements_ = {
requirements.adapterLuid.LowPart,
requirements.adapterLuid.HighPart,
static_cast<uint32_t>(requirements.minFeatureLevel),
};
requirements_queried_ = true;
std::ostringstream message;
message << "OpenXR D3D12 adapter LUID " << std::hex
<< static_cast<uint32_t>(requirements_.adapter_luid_high) << ':'
<< requirements_.adapter_luid_low << ", minimum feature level 0x"
<< requirements_.minimum_feature_level;
Log(OpenXRLogLevel::Info, message.str());
return true;
}
bool BindAurora(OpenXRRuntime& runtime) {
ClearError();
if (!requirements_queried_ || runtime_ != &runtime || runtime.HasSession()) {
return Fail("QueryGraphicsRequirements must succeed on this OpenXR instance before BindAurora");
}
if (bound_) {
return Fail("OpenXR D3D12 backend is already bound");
}
AuroraD3D12NativeHandles handles{};
if (!aurora_d3d12_get_native_handles(&handles) || handles.device == nullptr ||
handles.queue == nullptr) {
return Fail("Aurora did not expose a Dawn D3D12 device and command queue");
}
if (handles.adapterLuidLow != requirements_.adapter_luid_low ||
handles.adapterLuidHigh != requirements_.adapter_luid_high) {
std::ostringstream message;
message << "Aurora selected D3D12 adapter " << std::hex
<< static_cast<uint32_t>(handles.adapterLuidHigh) << ':'
<< handles.adapterLuidLow << ", but OpenXR requires "
<< static_cast<uint32_t>(requirements_.adapter_luid_high) << ':'
<< requirements_.adapter_luid_low;
return Fail(message.str());
}
auto* device = static_cast<ID3D12Device*>(handles.device);
const D3D_FEATURE_LEVEL minimum =
static_cast<D3D_FEATURE_LEVEL>(requirements_.minimum_feature_level);
D3D12_FEATURE_DATA_FEATURE_LEVELS levels{1, &minimum, D3D_FEATURE_LEVEL_1_0_CORE};
if (FAILED(device->CheckFeatureSupport(D3D12_FEATURE_FEATURE_LEVELS, &levels,
sizeof(levels))) ||
levels.MaxSupportedFeatureLevel < minimum) {
return Fail("Aurora's D3D12 device does not satisfy the OpenXR minimum feature level");
}
XrGraphicsBindingD3D12KHR binding{XR_TYPE_GRAPHICS_BINDING_D3D12_KHR};
binding.device = device;
binding.queue = static_cast<ID3D12CommandQueue*>(handles.queue);
if (!runtime.CreateSession(&binding)) {
return Fail("OpenXR rejected Aurora's D3D12 device/queue binding");
}
owns_session_ = true;
aurora_format_ = static_cast<DXGI_FORMAT>(handles.colorDxgiFormat);
if (!SelectSwapchainFormat() || !CreateSwapchains()) {
DestroySwapchains();
runtime.DestroySession();
owns_session_ = false;
return false;
}
if (runtime.ShouldExit()) {
DestroySwapchains();
runtime.DestroySession();
owns_session_ = false;
return Fail("OpenXR session became loss-pending while creating D3D12 swapchains");
}
if (!aurora_d3d12_enable_stereo_bridge(&Impl::OnAuroraSubmitted, this)) {
DestroySwapchains();
runtime.DestroySession();
owns_session_ = false;
return Fail("Aurora could not enable its zero-readback D3D12 stereo bridge");
}
bridge_enabled_ = true;
bound_ = true;
std::ostringstream message;
message << "OpenXR D3D12 swapchains ready: DXGI format "
<< static_cast<int64_t>(swapchain_format_) << ", eyes "
<< eye_swapchains_[0].width << 'x' << eye_swapchains_[0].height << " / "
<< eye_swapchains_[1].width << 'x' << eye_swapchains_[1].height;
Log(OpenXRLogLevel::Info, message.str());
return true;
}
OpenXRD3D12BeginStatus BeginFrame(const OpenXRD3D12Presentation& presentation,
OpenXRD3D12Frame& frame) {
frame = {};
frame.presentation = presentation;
if (!bound_ || runtime_ == nullptr) {
Fail("BeginFrame called before the D3D12 backend was bound");
return OpenXRD3D12BeginStatus::Error;
}
if (frame_active_) {
Fail("BeginFrame called while another OpenXR frame is active");
return OpenXRD3D12BeginStatus::Error;
}
const OpenXRFrameStatus status = runtime_->WaitFrame(frame.xr_frame);
if (status != OpenXRFrameStatus::Ready) {
if (status == OpenXRFrameStatus::Error) {
Fail(BeginStatusOperation(status));
}
switch (status) {
case OpenXRFrameStatus::SessionNotRunning:
return OpenXRD3D12BeginStatus::SessionNotRunning;
case OpenXRFrameStatus::ExitRequested:
return OpenXRD3D12BeginStatus::ExitRequested;
case OpenXRFrameStatus::Error:
return OpenXRD3D12BeginStatus::Error;
case OpenXRFrameStatus::Ready:
break;
}
}
if (!runtime_->BeginFrame(frame.xr_frame)) {
Fail("xrBeginFrame failed");
return OpenXRD3D12BeginStatus::Error;
}
frame_active_ = true;
active_frame_serial_ = frame.xr_frame.serial;
active_frame_ = frame.xr_frame;
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
frame.render_width[eye] = eye_swapchains_[eye].width;
frame.render_height[eye] = eye_swapchains_[eye].height;
}
if (!frame.xr_frame.should_render) {
return OpenXRD3D12BeginStatus::Ready;
}
if (!runtime_->LocateViews(frame.xr_frame)) {
Fail("xrLocateViews failed");
EndActiveFrameWithoutLayers(frame.xr_frame);
return OpenXRD3D12BeginStatus::Error;
}
active_frame_ = frame.xr_frame;
if (!frame.xr_frame.views_valid) {
return OpenXRD3D12BeginStatus::Ready;
}
const uint32_t target_count =
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];
}
std::array<AuroraD3D12StereoTarget, kOpenXREyeCount> targets{};
for (uint32_t eye = 0; eye < target_count; ++eye) {
auto& swapchain = eye_swapchains_[eye];
if (!AcquireSwapchain(swapchain)) {
ReleaseAcquiredSwapchains();
EndActiveFrameWithoutLayers(frame.xr_frame);
return OpenXRD3D12BeginStatus::Error;
}
targets[eye] = {
swapchain.images[swapchain.acquired_index].texture,
swapchain.width,
swapchain.height,
static_cast<int64_t>(swapchain_format_),
};
}
{
std::lock_guard lock(submission_mutex_);
awaiting_token_ = frame.xr_frame.serial;
submitted_token_ = 0;
submission_arrived_ = false;
submission_success_ = false;
submission_unsafe_ = false;
}
if (!aurora_d3d12_set_stereo_targets(frame.xr_frame.serial, targets.data(), target_count)) {
{
std::lock_guard lock(submission_mutex_);
awaiting_token_ = 0;
}
ReleaseAcquiredSwapchains();
Fail("Aurora rejected the acquired OpenXR D3D12 swapchain target");
EndActiveFrameWithoutLayers(frame.xr_frame);
return OpenXRD3D12BeginStatus::Error;
}
frame.expects_gpu_submission = true;
return OpenXRD3D12BeginStatus::Ready;
}
OpenXRD3D12SubmissionStatus WaitForSubmission(const OpenXRD3D12Frame& frame,
uint32_t timeout_ms) {
if (!frame.expects_gpu_submission) {
return OpenXRD3D12SubmissionStatus::Success;
}
std::unique_lock lock(submission_mutex_);
const auto ready = [&] {
return shutting_down_ ||
(submission_arrived_ && submitted_token_ == frame.xr_frame.serial);
};
if (timeout_ms == std::numeric_limits<uint32_t>::max()) {
submission_cv_.wait(lock, ready);
} else if (!submission_cv_.wait_for(lock, std::chrono::milliseconds(timeout_ms), ready)) {
return OpenXRD3D12SubmissionStatus::Timeout;
}
if (shutting_down_) {
return OpenXRD3D12SubmissionStatus::ShuttingDown;
}
return submission_success_ ? OpenXRD3D12SubmissionStatus::Success
: OpenXRD3D12SubmissionStatus::Failed;
}
bool TryCancelPendingFrame(OpenXRD3D12Frame& frame) {
if (!frame_active_ || !frame.expects_gpu_submission ||
frame.xr_frame.serial != active_frame_serial_) {
return false;
}
if (!aurora_d3d12_cancel_stereo_targets(frame.xr_frame.serial)) {
return false;
}
// A successful bridge cancellation is serialized against Encode and
// never generates a callback, so this token has no GPU ownership.
std::lock_guard lock(submission_mutex_);
awaiting_token_ = 0;
submitted_token_ = 0;
submission_arrived_ = false;
submission_success_ = false;
submission_unsafe_ = false;
frame.expects_gpu_submission = false;
return true;
}
bool FinishFrame(OpenXRD3D12Frame& frame, bool submit_layer) {
if (!frame_active_ || runtime_ == nullptr ||
frame.xr_frame.serial != active_frame_serial_) {
return Fail("FinishFrame received a stale or inactive OpenXR frame token");
}
bool submission_unsafe = false;
{
std::lock_guard lock(submission_mutex_);
submission_unsafe = submission_arrived_ &&
submitted_token_ == frame.xr_frame.serial &&
submission_unsafe_;
}
if (submission_unsafe) {
AbandonAcquiredSwapchains();
Fail("Aurora's D3D12 stereo submission failed after GPU work may have been queued");
}
bool release_ok = ReleaseAcquiredSwapchains();
const bool position_valid =
(frame.xr_frame.view_state_flags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0;
const bool composition_pose_valid =
frame.presentation.mode == OpenXRD3D12FrameMode::VirtualScreen || position_valid;
const bool can_submit = submit_layer && release_ok && frame.xr_frame.should_render &&
frame.xr_frame.views_valid && frame.expects_gpu_submission &&
composition_pose_valid;
bool end_ok = false;
if (!runtime_->IsSessionRunning()) {
// A session that is no longer running needs no compositor frame
// completion call. Preserve the original backend failure instead
// of replacing it with a stale-token error.
end_ok = true;
} else if (!can_submit) {
end_ok = runtime_->EndFrameWithoutLayers(frame.xr_frame);
} else if (frame.presentation.mode == OpenXRD3D12FrameMode::VirtualScreen) {
XrCompositionLayerQuad quad{XR_TYPE_COMPOSITION_LAYER_QUAD};
quad.layerFlags = 0;
quad.space = runtime_->ViewSpace();
quad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
quad.subImage.swapchain = eye_swapchains_[0].handle;
quad.subImage.imageRect = {{0, 0},
{static_cast<int32_t>(eye_swapchains_[0].width),
static_cast<int32_t>(eye_swapchains_[0].height)}};
quad.subImage.imageArrayIndex = 0;
quad.pose.orientation = {0.0f, 0.0f, 0.0f, 1.0f};
quad.pose.position = {0.0f, 0.0f, -std::max(0.25f, frame.presentation.quad_distance_meters)};
quad.size.width = std::max(0.25f, frame.presentation.quad_width_meters);
quad.size.height = quad.size.width * static_cast<float>(eye_swapchains_[0].height) /
static_cast<float>(eye_swapchains_[0].width);
const XrCompositionLayerBaseHeader* layers[] = {
reinterpret_cast<const XrCompositionLayerBaseHeader*>(&quad)};
end_ok = runtime_->EndFrame(frame.xr_frame, layers, 1);
} else {
std::array<XrCompositionLayerProjectionView, kOpenXREyeCount> views{};
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
views[eye] = {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW};
views[eye].pose.orientation = frame.xr_frame.views[eye].pose.orientation;
views[eye].pose.position = position_valid
? frame.xr_frame.views[eye].pose.position
: XrVector3f{0.0f, 0.0f, 0.0f};
views[eye].fov = frame.xr_frame.views[eye].fov;
views[eye].subImage.swapchain = eye_swapchains_[eye].handle;
views[eye].subImage.imageRect = {
{0, 0},
{static_cast<int32_t>(eye_swapchains_[eye].width),
static_cast<int32_t>(eye_swapchains_[eye].height)}};
views[eye].subImage.imageArrayIndex = 0;
}
XrCompositionLayerProjection projection{XR_TYPE_COMPOSITION_LAYER_PROJECTION};
projection.layerFlags = 0;
projection.space = runtime_->AppSpace();
projection.viewCount = kOpenXREyeCount;
projection.views = views.data();
const XrCompositionLayerBaseHeader* layers[] = {
reinterpret_cast<const XrCompositionLayerBaseHeader*>(&projection)};
end_ok = runtime_->EndFrame(frame.xr_frame, layers, 1);
}
frame_active_ = false;
active_frame_serial_ = 0;
active_frame_ = {};
frame.expects_gpu_submission = false;
{
std::lock_guard lock(submission_mutex_);
awaiting_token_ = 0;
submission_arrived_ = false;
submission_success_ = false;
submission_unsafe_ = false;
}
return release_ok && end_ok;
}
bool Shutdown() {
if (shutdown_unsafe_) {
return false;
}
{
std::lock_guard lock(submission_mutex_);
shutting_down_ = true;
}
submission_cv_.notify_all();
bool bridge_drained = true;
if (bridge_enabled_) {
bridge_drained = aurora_d3d12_disable_stereo_bridge();
bridge_enabled_ = false;
}
if (!bridge_drained) {
AbandonAcquiredSwapchains();
shutdown_unsafe_ = true;
Fail("D3D12 queue completion is unknown; retaining the OpenXR session and graphics owners");
return false;
}
// A queue-tail fence proved that no bridge command can still reference
// an acquired image. This also makes a conservatively abandoned image
// releasable after an earlier submission failure.
AllowAcquiredSwapchainsAfterGpuDrain();
ReleaseAcquiredSwapchains();
if (frame_active_ && runtime_ != nullptr) {
// Shutdown is required to run on the XR owner thread after Aurora's
// worker is idle, so it is safe to close an abandoned frame here.
if (runtime_->IsSessionRunning()) {
runtime_->EndFrameWithoutLayers(active_frame_);
}
frame_active_ = false;
active_frame_serial_ = 0;
active_frame_ = {};
}
DestroySwapchains();
if (owns_session_ && runtime_ != nullptr) {
runtime_->DestroySession();
owns_session_ = false;
}
bound_ = false;
requirements_queried_ = false;
runtime_ = nullptr;
return true;
}
bool IsBound() const { return bound_; }
const OpenXRD3D12GraphicsRequirements& GraphicsRequirements() const { return requirements_; }
int64_t SwapchainFormat() const { return static_cast<int64_t>(swapchain_format_); }
const std::string& LastError() const { return last_error_; }
private:
bool SelectSwapchainFormat() {
const auto& formats = runtime_->SwapchainFormats();
// Aurora's UNORM target contains the gamma-encoded bytes expected by
// the desktop compositor. OpenXR must declare the compatible sRGB
// sibling so the headset compositor decodes those raw bytes instead
// of treating them as linear light (which appears severely washed out).
const DXGI_FORMAT srgb = SrgbSibling(aurora_format_);
if (srgb != DXGI_FORMAT_UNKNOWN &&
std::find(formats.begin(), formats.end(), static_cast<int64_t>(srgb)) !=
formats.end()) {
swapchain_format_ = srgb;
return true;
}
const auto exact = std::find(formats.begin(), formats.end(), static_cast<int64_t>(aurora_format_));
if (exact != formats.end()) {
swapchain_format_ = aurora_format_;
return true;
}
const auto compatible = std::find_if(formats.begin(), formats.end(), [&](int64_t format) {
return SameDxgiCopyFamily(aurora_format_, static_cast<DXGI_FORMAT>(format));
});
if (compatible == formats.end()) {
return Fail("OpenXR offered no swapchain format copy-compatible with Aurora's D3D12 color format");
}
swapchain_format_ = static_cast<DXGI_FORMAT>(*compatible);
return true;
}
bool CreateSwapchains() {
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
const auto& view = runtime_->ViewConfiguration()[eye];
auto& swapchain = eye_swapchains_[eye];
swapchain.width = view.render_width;
swapchain.height = view.render_height;
XrSwapchainCreateInfo create{XR_TYPE_SWAPCHAIN_CREATE_INFO};
create.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT |
XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
if (IsSrgbFormat(swapchain_format_)) {
// Matches DolphinXR's raw-UNORM-write/sRGB-compositor path and
// asks D3D runtimes to expose a typeless-compatible resource.
create.usageFlags |= XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT;
}
create.format = static_cast<int64_t>(swapchain_format_);
create.sampleCount = 1;
create.width = swapchain.width;
create.height = swapchain.height;
create.faceCount = 1;
create.arraySize = 1;
create.mipCount = 1;
XrResult result = xrCreateSwapchain(runtime_->Session(), &create, &swapchain.handle);
ObserveResult(result);
if (XR_FAILED(result)) {
std::ostringstream message;
message << "xrCreateSwapchain failed for D3D12 eye " << eye << " (" << result << ')';
return Fail(message.str());
}
uint32_t count = 0;
result = xrEnumerateSwapchainImages(swapchain.handle, 0, &count, nullptr);
ObserveResult(result);
if (XR_FAILED(result) || count == 0) {
return Fail("OpenXR returned no D3D12 swapchain images");
}
swapchain.images.resize(count);
for (auto& image : swapchain.images) {
image = {XR_TYPE_SWAPCHAIN_IMAGE_D3D12_KHR};
}
result = xrEnumerateSwapchainImages(
swapchain.handle, count, &count,
reinterpret_cast<XrSwapchainImageBaseHeader*>(swapchain.images.data()));
ObserveResult(result);
if (XR_FAILED(result)) {
return Fail("xrEnumerateSwapchainImages failed for a D3D12 eye swapchain");
}
}
return true;
}
bool AcquireSwapchain(EyeSwapchain& swapchain) {
XrSwapchainImageAcquireInfo acquire{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
XrResult result = xrAcquireSwapchainImage(swapchain.handle, &acquire,
&swapchain.acquired_index);
ObserveResult(result);
if (XR_FAILED(result)) {
return Fail("xrAcquireSwapchainImage failed for a D3D12 eye swapchain");
}
swapchain.acquired = true;
swapchain.waited = false;
swapchain.release_forbidden = false;
XrSwapchainImageWaitInfo wait{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
wait.timeout = XR_INFINITE_DURATION;
result = xrWaitSwapchainImage(swapchain.handle, &wait);
ObserveResult(result);
if (result == XR_TIMEOUT_EXPIRED) {
return Fail("xrWaitSwapchainImage unexpectedly timed out for a D3D12 eye swapchain");
}
if (XR_FAILED(result)) {
return Fail("xrWaitSwapchainImage failed for a D3D12 eye swapchain");
}
swapchain.waited = true;
if (swapchain.acquired_index >= swapchain.images.size()) {
return Fail("OpenXR returned an out-of-range D3D12 swapchain image index");
}
return true;
}
bool ReleaseAcquiredSwapchains() {
bool success = true;
for (auto& swapchain : eye_swapchains_) {
if (!swapchain.acquired || swapchain.handle == XR_NULL_HANDLE) {
continue;
}
if (!swapchain.waited) {
// OpenXR only permits release after a successful wait. Keep the
// image acquired and let session teardown destroy the child.
success = false;
Log(OpenXRLogLevel::Warning,
"cannot release an OpenXR D3D12 image whose wait did not complete");
continue;
}
if (swapchain.release_forbidden) {
// Aurora reported a failed submission after it may already
// have queued native D3D12 work. Without a trustworthy fence,
// xrReleaseSwapchainImage could race that work. Leave the image
// acquired and let xrDestroySession reclaim the child instead.
success = false;
Log(OpenXRLogLevel::Warning,
"deferring an OpenXR D3D12 image after an unsafe GPU submission");
continue;
}
XrSwapchainImageReleaseInfo release{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
const XrResult result = xrReleaseSwapchainImage(swapchain.handle, &release);
ObserveResult(result);
if (XR_FAILED(result)) {
success = false;
Fail("xrReleaseSwapchainImage failed for a D3D12 eye swapchain");
continue;
}
swapchain.acquired = false;
swapchain.waited = false;
}
return success;
}
void AbandonAcquiredSwapchains() noexcept {
for (auto& swapchain : eye_swapchains_) {
if (swapchain.acquired) {
swapchain.release_forbidden = true;
}
}
}
void AllowAcquiredSwapchainsAfterGpuDrain() noexcept {
for (auto& swapchain : eye_swapchains_) {
if (swapchain.acquired) {
swapchain.release_forbidden = false;
}
}
}
void DestroySwapchains() {
for (auto& swapchain : eye_swapchains_) {
if (swapchain.handle != XR_NULL_HANDLE && !swapchain.acquired) {
xrDestroySwapchain(swapchain.handle);
} else if (swapchain.acquired) {
Log(OpenXRLogLevel::Warning,
"D3D12 swapchain still owns an acquired image; deferring its destruction to xrDestroySession");
}
swapchain = {};
}
swapchain_format_ = DXGI_FORMAT_UNKNOWN;
}
void EndActiveFrameWithoutLayers(const OpenXRFrame& frame) {
if (runtime_ != nullptr && runtime_->IsSessionRunning()) {
runtime_->EndFrameWithoutLayers(frame);
}
frame_active_ = false;
active_frame_serial_ = 0;
active_frame_ = {};
}
void ObserveResult(XrResult result) noexcept {
if (runtime_ != nullptr) {
runtime_->ObserveResult(result);
}
}
static void OnAuroraSubmitted(uint64_t token, bool success, void* userdata) {
auto* self = static_cast<Impl*>(userdata);
if (self == nullptr) {
return;
}
{
std::lock_guard lock(self->submission_mutex_);
if (token != self->awaiting_token_) {
return;
}
self->submitted_token_ = token;
self->submission_success_ = success;
self->submission_arrived_ = true;
self->submission_unsafe_ = !success;
}
self->submission_cv_.notify_all();
}
bool Fail(std::string message) {
last_error_ = std::move(message);
Log(OpenXRLogLevel::Error, last_error_);
return false;
}
void ClearError() { last_error_.clear(); }
void Log(OpenXRLogLevel level, std::string_view message) const noexcept {
if (!logger_) {
return;
}
try {
logger_(level, message);
} catch (...) {
}
}
OpenXRRuntime* runtime_ = nullptr;
OpenXRLogCallback logger_;
OpenXRD3D12GraphicsRequirements requirements_{};
std::array<EyeSwapchain, kOpenXREyeCount> eye_swapchains_{};
DXGI_FORMAT aurora_format_ = DXGI_FORMAT_UNKNOWN;
DXGI_FORMAT swapchain_format_ = DXGI_FORMAT_UNKNOWN;
std::string last_error_;
std::mutex submission_mutex_;
std::condition_variable submission_cv_;
uint64_t awaiting_token_ = 0;
uint64_t submitted_token_ = 0;
bool submission_arrived_ = false;
bool submission_success_ = false;
bool submission_unsafe_ = false;
bool shutting_down_ = false;
uint64_t active_frame_serial_ = 0;
OpenXRFrame active_frame_{};
bool requirements_queried_ = false;
bool owns_session_ = false;
bool bridge_enabled_ = false;
bool bound_ = false;
bool frame_active_ = false;
bool shutdown_unsafe_ = false;
};
OpenXRD3D12Backend::OpenXRD3D12Backend(OpenXRLogCallback logger)
: m_impl(std::make_unique<Impl>(std::move(logger))) {}
OpenXRD3D12Backend::~OpenXRD3D12Backend() = default;
bool OpenXRD3D12Backend::QueryGraphicsRequirements(OpenXRRuntime& runtime) {
return m_impl->QueryGraphicsRequirements(runtime);
}
bool OpenXRD3D12Backend::BindAurora(OpenXRRuntime& runtime) {
return m_impl->BindAurora(runtime);
}
OpenXRD3D12BeginStatus OpenXRD3D12Backend::BeginFrame(
const OpenXRD3D12Presentation& presentation, OpenXRD3D12Frame& frame) {
return m_impl->BeginFrame(presentation, frame);
}
OpenXRD3D12SubmissionStatus OpenXRD3D12Backend::WaitForSubmission(
const OpenXRD3D12Frame& frame, uint32_t timeout_ms) {
return m_impl->WaitForSubmission(frame, timeout_ms);
}
bool OpenXRD3D12Backend::TryCancelPendingFrame(OpenXRD3D12Frame& frame) {
return m_impl->TryCancelPendingFrame(frame);
}
bool OpenXRD3D12Backend::FinishFrame(OpenXRD3D12Frame& frame, bool submit_layer) {
return m_impl->FinishFrame(frame, submit_layer);
}
bool OpenXRD3D12Backend::Shutdown() { return m_impl->Shutdown(); }
bool OpenXRD3D12Backend::IsBound() const { return m_impl->IsBound(); }
const OpenXRD3D12GraphicsRequirements& OpenXRD3D12Backend::GraphicsRequirements() const {
return m_impl->GraphicsRequirements();
}
int64_t OpenXRD3D12Backend::SwapchainFormat() const { return m_impl->SwapchainFormat(); }
const std::string& OpenXRD3D12Backend::LastError() const { return m_impl->LastError(); }
} // namespace mkw::vr
#endif // defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
+731
View File
@@ -0,0 +1,731 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#if defined(_WIN32) && !defined(NOMINMAX)
#define NOMINMAX
#endif
#include "vr/openxr_integration.h"
#include "runtime_config.h"
#include "runtime_log.h"
#include "vr/mkw_vr_policy.h"
#include "vr/mkw_vr_instrumentation.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <condition_variable>
#include <cstdint>
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#if defined(MKW_ENABLE_OPENXR)
#include "vr/openxr_runtime.h"
#if defined(_WIN32)
#include "vr/openxr_d3d12.h"
#else
#include "vr/openxr_vulkan_backend.h"
#endif
#endif
namespace mkw::vr {
namespace {
void ConfigurePolicy(bool enabled) noexcept {
MkwVRPolicyReset();
MkwVRPolicyConfig config{};
config.enabled = enabled;
config.immersive_races = true;
config.world_units_per_meter = RuntimeConfigFile::VrWorldUnitsPerMeter(500.0f);
config.hud_distance_meters = RuntimeConfigFile::VrHudDistanceMeters(2.0f);
MkwVRPolicyConfigure(config);
MkwVRInstrumentationInitialize();
}
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
struct Quaternion {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
float w = 1.0f;
};
struct Pose {
Quaternion orientation{};
std::array<float, 3> position{};
};
Quaternion Normalize(Quaternion value) noexcept {
const float length_squared = value.x * value.x + value.y * value.y +
value.z * value.z + value.w * value.w;
if (!(length_squared > 1.0e-12f)) {
return {};
}
const float inverse_length = 1.0f / std::sqrt(length_squared);
value.x *= inverse_length;
value.y *= inverse_length;
value.z *= inverse_length;
value.w *= inverse_length;
return value;
}
Quaternion Conjugate(Quaternion value) noexcept {
return {-value.x, -value.y, -value.z, value.w};
}
Quaternion Multiply(const Quaternion& left, const Quaternion& right) noexcept {
return Normalize({
left.w * right.x + left.x * right.w + left.y * right.z - left.z * right.y,
left.w * right.y - left.x * right.z + left.y * right.w + left.z * right.x,
left.w * right.z + left.x * right.y - left.y * right.x + left.z * right.w,
left.w * right.w - left.x * right.x - left.y * right.y - left.z * right.z,
});
}
std::array<float, 3> Rotate(const Quaternion& q, const std::array<float, 3>& value) noexcept {
// Expanded q * [v,0] * conjugate(q), avoiding two temporary normalizations.
const float tx = 2.0f * (q.y * value[2] - q.z * value[1]);
const float ty = 2.0f * (q.z * value[0] - q.x * value[2]);
const float tz = 2.0f * (q.x * value[1] - q.y * value[0]);
return {
value[0] + q.w * tx + (q.y * tz - q.z * ty),
value[1] + q.w * ty + (q.z * tx - q.x * tz),
value[2] + q.w * tz + (q.x * ty - q.y * tx),
};
}
void RotationMatrix(const Quaternion& value, float matrix[9]) noexcept {
const Quaternion q = Normalize(value);
const float xx = q.x * q.x;
const float yy = q.y * q.y;
const float zz = q.z * q.z;
const float xy = q.x * q.y;
const float xz = q.x * q.z;
const float yz = q.y * q.z;
const float wx = q.w * q.x;
const float wy = q.w * q.y;
const float wz = q.w * q.z;
matrix[0] = 1.0f - 2.0f * (yy + zz);
matrix[1] = 2.0f * (xy - wz);
matrix[2] = 2.0f * (xz + wy);
matrix[3] = 2.0f * (xy + wz);
matrix[4] = 1.0f - 2.0f * (xx + zz);
matrix[5] = 2.0f * (yz - wx);
matrix[6] = 2.0f * (xz - wy);
matrix[7] = 2.0f * (yz + wx);
matrix[8] = 1.0f - 2.0f * (xx + yy);
}
Pose CenterPose(const OpenXRFrame& frame, bool position_valid) noexcept {
const auto& left = frame.views[0].pose;
const auto& right = frame.views[1].pose;
Quaternion l{left.orientation.x, left.orientation.y, left.orientation.z,
left.orientation.w};
Quaternion r{right.orientation.x, right.orientation.y, right.orientation.z,
right.orientation.w};
l = Normalize(l);
r = Normalize(r);
const float dot = l.x * r.x + l.y * r.y + l.z * r.z + l.w * r.w;
if (dot < 0.0f) {
r = {-r.x, -r.y, -r.z, -r.w};
}
Pose center;
center.orientation = Normalize({l.x + r.x, l.y + r.y, l.z + r.z, l.w + r.w});
if (position_valid) {
center.position = {
(left.position.x + right.position.x) * 0.5f,
(left.position.y + right.position.y) * 0.5f,
(left.position.z + right.position.z) * 0.5f,
};
}
return center;
}
void IdentityEye(AuroraStereoEye& eye) noexcept {
std::fill(std::begin(eye.projection), std::end(eye.projection), 0.0f);
eye.projection[0] = 1.0f;
eye.projection[5] = 1.0f;
eye.projection[10] = 1.0f;
eye.projection[15] = 1.0f;
std::fill(std::begin(eye.viewFromCenter), std::end(eye.viewFromCenter), 0.0f);
eye.viewFromCenter[0] = 1.0f;
eye.viewFromCenter[5] = 1.0f;
eye.viewFromCenter[10] = 1.0f;
}
void ProjectionFromFov(const XrFovf& fov, float output[16]) noexcept {
const float left = std::tan(fov.angleLeft);
const float right = std::tan(fov.angleRight);
const float down = std::tan(fov.angleDown);
const float up = std::tan(fov.angleUp);
const float inverse_width = 1.0f / (right - left);
const float inverse_height = 1.0f / (up - down);
std::fill(output, output + 16, 0.0f);
output[0] = 2.0f * inverse_width;
output[2] = (right + left) * inverse_width;
output[5] = 2.0f * inverse_height;
output[6] = (up + down) * inverse_height;
}
void ViewFromBase(const XrPosef& eye_pose, const Pose& base, bool position_valid,
float units_per_meter, float output[12]) noexcept {
const Quaternion eye = Normalize({eye_pose.orientation.x, eye_pose.orientation.y,
eye_pose.orientation.z, eye_pose.orientation.w});
const Quaternion inverse_eye = Conjugate(eye);
const Quaternion delta = Multiply(inverse_eye, base.orientation);
float rotation[9];
RotationMatrix(delta, rotation);
std::array<float, 3> translation{};
if (position_valid) {
const std::array<float, 3> base_to_eye{
base.position[0] - eye_pose.position.x,
base.position[1] - eye_pose.position.y,
base.position[2] - eye_pose.position.z,
};
translation = Rotate(inverse_eye, base_to_eye);
}
output[0] = rotation[0];
output[1] = rotation[1];
output[2] = rotation[2];
output[3] = translation[0] * units_per_meter;
output[4] = rotation[3];
output[5] = rotation[4];
output[6] = rotation[5];
output[7] = translation[1] * units_per_meter;
output[8] = rotation[6];
output[9] = rotation[7];
output[10] = rotation[8];
output[11] = translation[2] * units_per_meter;
}
class OpenXRIntegration final {
public:
static OpenXRIntegration& Get() {
static OpenXRIntegration integration;
return integration;
}
OpenXRStartupResult Prepare(AuroraConfig& aurora_config) {
Shutdown();
{
std::lock_guard lock(error_mutex_);
last_error_.clear();
}
if (graphics_retained_) {
SetError("OpenXR cannot be restarted after an unfenceable D3D12 submission");
return OpenXRStartupResult::Unavailable;
}
requested_ = RuntimeConfigFile::VrEnabled(false);
ConfigurePolicy(requested_);
if (!requested_) {
return OpenXRStartupResult::Disabled;
}
if (aurora_config.desiredBackend != BACKEND_AUTO &&
aurora_config.desiredBackend != BACKEND_D3D12) {
SetError("OpenXR currently requires the D3D12 graphics backend on Windows");
return OpenXRStartupResult::Unavailable;
}
logger_ = [](OpenXRLogLevel level, std::string_view message) {
const char* name = level == OpenXRLogLevel::Error ? "error" :
level == OpenXRLogLevel::Warning ? "warning" : "info";
RT_LOG(RT_TAG_RUNTIME) << "[openxr::" << name << "] " << message << std::endl;
};
runtime_ = std::make_unique<OpenXRRuntime>(logger_);
backend_ = std::make_unique<OpenXRD3D12Backend>(logger_);
OpenXRConfig config{};
config.application_name = aurora_config.appName != nullptr ? aurora_config.appName
: "WiiCompiled";
config.engine_name = "Aurora";
config.resolution_scale = RuntimeConfigFile::VrRenderScale(1.0f);
config.required_extensions = {"XR_KHR_D3D12_enable"};
if (!runtime_->Initialize(config)) {
SetError("OpenXR instance initialization failed: " + runtime_->LastError().message);
ResetPreparedObjects();
return OpenXRStartupResult::Unavailable;
}
if (!backend_->QueryGraphicsRequirements(*runtime_)) {
SetError(backend_->LastError());
ResetPreparedObjects();
return OpenXRStartupResult::Unavailable;
}
const auto& requirements = backend_->GraphicsRequirements();
aurora_config.desiredBackend = BACKEND_D3D12;
aurora_config.xrInterop = true;
aurora_config.hasD3D12AdapterLuid = true;
aurora_config.d3d12AdapterLuidLow = requirements.adapter_luid_low;
aurora_config.d3d12AdapterLuidHigh = requirements.adapter_luid_high;
prepared_ = true;
return OpenXRStartupResult::Prepared;
}
bool Start(AuroraBackend active_backend) {
if (!prepared_ || runtime_ == nullptr || backend_ == nullptr) {
return !requested_;
}
if (active_backend != BACKEND_D3D12) {
SetError("Aurora could not create the OpenXR-required D3D12 backend");
ResetPreparedObjects();
return false;
}
if (!backend_->BindAurora(*runtime_)) {
SetError(backend_->LastError());
ResetPreparedObjects();
return false;
}
stop_.store(false, std::memory_order_release);
teardown_requested_.store(false, std::memory_order_release);
WithdrawPublishedFrame();
aurora_set_stereo_frame_provider(&OpenXRIntegration::ProvideStereoFrame, this);
provider_registered_ = true;
running_.store(true, std::memory_order_release);
try {
pacing_thread_ = std::thread([this] { PacingThread(); });
} catch (const std::exception& exception) {
running_.store(false, std::memory_order_release);
aurora_set_stereo_frame_provider(nullptr, nullptr);
provider_registered_ = false;
SetError(std::string("could not start the OpenXR pacing thread: ") + exception.what());
ResetPreparedObjects();
return false;
}
RT_LOG(RT_TAG_RUNTIME) << "OpenXR asynchronous D3D12 presentation started" << std::endl;
return true;
}
void Shutdown() noexcept {
teardown_requested_.store(false, std::memory_order_release);
if (pacing_thread_.joinable()) {
// Registration changes are only safe while no sealed frame is in
// flight. The caller invokes us before Aurora teardown.
aurora_quiesce_frame_worker();
aurora_set_stereo_frame_provider(nullptr, nullptr);
provider_registered_ = false;
WithdrawPublishedFrame();
{
// Pair the predicate update with the wait mutex. Otherwise a
// terminal pacing thread can observe false, miss the notify,
// and make join wait forever.
std::lock_guard lock(stop_mutex_);
stop_.store(true, std::memory_order_release);
}
stop_cv_.notify_all();
pacing_thread_.join();
} else {
if (provider_registered_) {
aurora_quiesce_frame_worker();
aurora_set_stereo_frame_provider(nullptr, nullptr);
provider_registered_ = false;
}
ShutdownOrRetainGraphicsObjects();
}
running_.store(false, std::memory_order_release);
MkwVRPolicySetSessionActive(false);
backend_.reset();
runtime_.reset();
prepared_ = false;
ResetTrackingOrigin();
applied_session_run_serial_ = 0;
session_was_active_ = false;
}
bool IsRunning() const noexcept { return running_.load(std::memory_order_acquire); }
void ServiceProducerFrameBoundary() noexcept {
if (teardown_requested_.load(std::memory_order_acquire)) {
Shutdown();
}
}
std::string LastError() const {
std::lock_guard lock(error_mutex_);
return last_error_;
}
private:
struct PublishedFrame {
AuroraStereoFrame frame{};
};
void ResetPreparedObjects() {
ShutdownOrRetainGraphicsObjects();
backend_.reset();
runtime_.reset();
prepared_ = false;
}
bool ShutdownOrRetainGraphicsObjects() noexcept {
if (backend_ != nullptr && !backend_->Shutdown()) {
RT_LOG(RT_TAG_RUNTIME)
<< "OpenXR D3D12 queue completion is unknown; retaining the backend, "
"runtime, session, and graphics resources until process exit"
<< std::endl;
(void)backend_.release();
(void)runtime_.release();
graphics_retained_ = true;
return false;
}
if (runtime_ != nullptr) {
runtime_->Shutdown();
}
return true;
}
static bool ProvideStereoFrame(uint32_t, AuroraStereoFrame* output, void* userdata) {
auto* self = static_cast<OpenXRIntegration*>(userdata);
if (self == nullptr || output == nullptr) {
return false;
}
// The packet storage is reused by the XR thread. Claim and copy it
// under one short lock so cancellation cannot begin the next packet
// while this callback is preempted between exchange and copy.
std::lock_guard lock(self->published_mutex_);
PublishedFrame* frame = self->published_.exchange(nullptr, std::memory_order_acq_rel);
if (frame == nullptr) {
return false;
}
*output = frame->frame;
return true;
}
void PacingThread() noexcept {
bool fatal = false;
bool presentation_logged = false;
VRPresentationMode logged_presentation = VRPresentationMode::Desktop;
uint32_t presentation_log_count = 0;
bool immersive_submission_logged = false;
while (!stop_.load(std::memory_order_acquire) && !fatal) {
const OpenXREventStatus events = runtime_->PollEvents();
const bool session_active = runtime_->IsSessionRunning();
MkwVRPolicySetSessionActive(session_active);
const uint64_t session_run_serial = runtime_->SessionRunSerial();
if (session_run_serial != applied_session_run_serial_) {
applied_session_run_serial_ = session_run_serial;
ResetTrackingOrigin();
}
if (session_active != session_was_active_) {
session_was_active_ = session_active;
if (!session_active) {
ResetTrackingOrigin();
}
}
if (events == OpenXREventStatus::ExitRequested) {
SetError("OpenXR runtime requested session exit; continuing on the desktop mirror");
break;
}
if (events == OpenXREventStatus::Error) {
SetError("OpenXR event processing failed: " + runtime_->LastError().message);
break;
}
if (!session_active) {
WaitForStopOrDelay(std::chrono::milliseconds(5));
continue;
}
const MkwVRPolicySnapshot policy = MkwVRPolicyGetSnapshot();
if ((!presentation_logged || policy.presentation != logged_presentation) &&
presentation_log_count < 16) {
presentation_logged = true;
logged_presentation = policy.presentation;
++presentation_log_count;
RT_LOG(RT_TAG_RUNTIME)
<< "[mkw-vr] presentation="
<< (policy.presentation == VRPresentationMode::ImmersiveRace
? "immersive-race"
: policy.presentation == VRPresentationMode::VirtualScreen
? "virtual-screen"
: "desktop")
<< ", scene=" << static_cast<unsigned>(policy.scene.mode)
<< ", screens=" << policy.scene.local_player_count
<< ", camera-valid=" << policy.camera.valid
<< ", scene-frame=" << policy.scene.guest_frame_index
<< ", camera-frame=" << policy.camera.guest_frame_index
<< ", bindings=0x" << std::hex << policy.available_bindings
<< std::dec << std::endl;
}
OpenXRD3D12Presentation presentation{};
const bool immersive = policy.presentation == VRPresentationMode::ImmersiveRace;
presentation.mode = immersive ? OpenXRD3D12FrameMode::ImmersiveProjection
: OpenXRD3D12FrameMode::VirtualScreen;
presentation.quad_distance_meters = policy.config.hud_distance_meters;
OpenXRD3D12Frame frame{};
const OpenXRD3D12BeginStatus begin = backend_->BeginFrame(presentation, frame);
if (begin == OpenXRD3D12BeginStatus::SessionNotRunning) {
MkwVRPolicySetSessionActive(false);
continue;
}
if (begin == OpenXRD3D12BeginStatus::ExitRequested) {
SetError("OpenXR runtime requested session exit; continuing on the desktop mirror");
break;
}
if (begin == OpenXRD3D12BeginStatus::Error) {
SetError(backend_->LastError());
fatal = true;
break;
}
if (!frame.expects_gpu_submission) {
if (!backend_->FinishFrame(frame, false)) {
SetError(backend_->LastError());
fatal = true;
}
continue;
}
{
std::lock_guard lock(published_mutex_);
BuildPublishedFrame(frame, immersive, policy.config.world_units_per_meter,
policy.content_tag);
published_.store(&published_frame_, std::memory_order_release);
}
OpenXRD3D12SubmissionStatus submission = OpenXRD3D12SubmissionStatus::Timeout;
bool canceled_before_encode = false;
bool submission_stalled = false;
const auto submission_deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds(250);
while (!stop_.load(std::memory_order_acquire) &&
submission == OpenXRD3D12SubmissionStatus::Timeout) {
submission = backend_->WaitForSubmission(frame, 50);
if (submission == OpenXRD3D12SubmissionStatus::Timeout) {
// A pause, minimized window, or guest stall may leave no GX
// frame to consume this packet. Withdraw it, then cancel the
// matching bridge target only if Encode has not taken ownership.
WithdrawPublishedFrame();
canceled_before_encode = backend_->TryCancelPendingFrame(frame);
if (canceled_before_encode) {
break;
}
if (std::chrono::steady_clock::now() >= submission_deadline) {
// Cancellation may lose a race to completion. Recheck
// the callback-published predicate under the backend
// mutex before declaring a stall at the deadline.
submission = backend_->WaitForSubmission(frame, 0);
submission_stalled =
submission == OpenXRD3D12SubmissionStatus::Timeout;
break;
}
}
}
WithdrawPublishedFrame();
if (stop_.load(std::memory_order_acquire)) {
// Aurora has been drained by Shutdown(); backend shutdown below
// cancels its pending target, then either safely releases the
// XR image or retains the entire graph if GPU completion is unknown.
break;
}
if (canceled_before_encode) {
if (!backend_->FinishFrame(frame, false)) {
SetError(backend_->LastError());
fatal = true;
}
continue;
}
if (submission_stalled) {
SetError("Aurora did not complete the OpenXR stereo submission; "
"requesting a safe desktop fallback");
fatal = true;
break;
}
const bool submit = submission == OpenXRD3D12SubmissionStatus::Success;
if (!backend_->FinishFrame(frame, submit)) {
SetError(backend_->LastError());
fatal = true;
} else if (!submit) {
SetError("Aurora's D3D12 stereo copy failed; continuing on the desktop mirror");
fatal = true;
} else if (immersive && !immersive_submission_logged) {
immersive_submission_logged = true;
RT_LOG(RT_TAG_RUNTIME)
<< "[mkw-vr] first immersive packet consumed and submitted as "
"an OpenXR projection layer"
<< std::endl;
}
}
running_.store(false, std::memory_order_release);
MkwVRPolicySetSessionActive(false);
if (!stop_.load(std::memory_order_acquire)) {
// A runtime/backend failure can happen while Aurora is submitting.
// Ask the producer to reach a safe frame boundary, drain Aurora,
// and unregister the provider before this XR owner destroys state.
teardown_requested_.store(true, std::memory_order_release);
std::unique_lock lock(stop_mutex_);
stop_cv_.wait(lock, [this] { return stop_.load(std::memory_order_acquire); });
}
ShutdownOrRetainGraphicsObjects();
}
void BuildPublishedFrame(const OpenXRD3D12Frame& source, bool immersive,
float units_per_meter, uint64_t content_tag) noexcept {
ApplyPendingReferenceSpaceChange(source.xr_frame);
auto& destination = published_frame_.frame;
destination = {};
destination.frameToken = source.xr_frame.serial;
destination.contentTag = content_tag;
destination.mode = immersive ? AURORA_STEREO_FRAME_IMMERSIVE_REPLAY
: AURORA_STEREO_FRAME_VIRTUAL_SCREEN;
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
destination.eyes[eye].width = source.render_width[eye];
destination.eyes[eye].height = source.render_height[eye];
IdentityEye(destination.eyes[eye]);
}
if (!immersive) {
last_immersive_ = false;
return;
}
const bool position_valid =
(source.xr_frame.view_state_flags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0;
if (!base_pose_valid_ || !last_immersive_) {
base_pose_ = CenterPose(source.xr_frame, position_valid);
base_pose_valid_ = true;
base_position_valid_ = position_valid;
} else if (position_valid && !base_position_valid_) {
base_pose_.position = CenterPose(source.xr_frame, true).position;
base_position_valid_ = true;
}
last_immersive_ = true;
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
ProjectionFromFov(source.xr_frame.views[eye].fov,
destination.eyes[eye].projection);
ViewFromBase(source.xr_frame.views[eye].pose, base_pose_,
position_valid && base_position_valid_,
units_per_meter, destination.eyes[eye].viewFromCenter);
}
}
void ApplyPendingReferenceSpaceChange(const OpenXRFrame& frame) noexcept {
if (runtime_->ConsumeAppSpaceChangesThrough(frame.predicted_display_time)) {
ResetTrackingOrigin();
}
}
void ResetTrackingOrigin() noexcept {
base_pose_ = {};
base_pose_valid_ = false;
base_position_valid_ = false;
last_immersive_ = false;
}
void WaitForStopOrDelay(std::chrono::milliseconds delay) {
std::unique_lock lock(stop_mutex_);
stop_cv_.wait_for(lock, delay,
[this] { return stop_.load(std::memory_order_acquire); });
}
void WithdrawPublishedFrame() noexcept {
std::lock_guard lock(published_mutex_);
published_.store(nullptr, std::memory_order_release);
}
void SetError(std::string message) {
{
std::lock_guard lock(error_mutex_);
last_error_ = std::move(message);
}
RT_LOG(RT_TAG_RUNTIME) << "OpenXR: " << LastError() << std::endl;
}
OpenXRLogCallback logger_;
std::unique_ptr<OpenXRRuntime> runtime_;
std::unique_ptr<OpenXRD3D12Backend> backend_;
std::thread pacing_thread_;
std::atomic_bool stop_{false};
std::atomic_bool running_{false};
std::atomic_bool teardown_requested_{false};
std::atomic<PublishedFrame*> published_{nullptr};
PublishedFrame published_frame_{};
std::mutex published_mutex_;
std::mutex stop_mutex_;
std::condition_variable stop_cv_;
mutable std::mutex error_mutex_;
std::string last_error_;
Pose base_pose_{};
bool base_pose_valid_ = false;
bool base_position_valid_ = false;
bool last_immersive_ = false;
uint64_t applied_session_run_serial_ = 0;
bool session_was_active_ = false;
bool requested_ = false;
bool prepared_ = false;
bool provider_registered_ = false;
bool graphics_retained_ = false;
};
#endif // defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
} // namespace
OpenXRStartupResult OpenXRPrepareAurora(AuroraConfig& config) {
#if !defined(MKW_ENABLE_OPENXR)
(void)config;
ConfigurePolicy(false);
return RuntimeConfigFile::VrEnabled(false) ? OpenXRStartupResult::Unavailable
: OpenXRStartupResult::Disabled;
#elif defined(_WIN32)
return OpenXRIntegration::Get().Prepare(config);
#else
ConfigurePolicy(RuntimeConfigFile::VrEnabled(false));
if (!RuntimeConfigFile::VrEnabled(false)) {
return OpenXRStartupResult::Disabled;
}
const OpenXRVulkanCapabilityInfo capability = OpenXRVulkanBackend::DawnInteropCapability();
RT_LOG(RT_TAG_RUNTIME) << "OpenXR Vulkan unavailable: " << capability.reason << std::endl;
return OpenXRStartupResult::Unavailable;
#endif
}
bool OpenXRStartAfterAurora(AuroraBackend active_backend) {
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
return OpenXRIntegration::Get().Start(active_backend);
#else
(void)active_backend;
return !RuntimeConfigFile::VrEnabled(false);
#endif
}
void OpenXRShutdownBeforeAurora() noexcept {
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
OpenXRIntegration::Get().Shutdown();
#else
MkwVRPolicySetSessionActive(false);
#endif
}
void OpenXRServiceProducerFrameBoundary() noexcept {
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
OpenXRIntegration::Get().ServiceProducerFrameBoundary();
#endif
}
bool OpenXRIsRunning() noexcept {
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
return OpenXRIntegration::Get().IsRunning();
#else
return false;
#endif
}
std::string OpenXRLastError() {
#if !defined(MKW_ENABLE_OPENXR)
return "this build was compiled without OpenXR support";
#elif defined(_WIN32)
return OpenXRIntegration::Get().LastError();
#else
return OpenXRVulkanBackend::DawnInteropCapability().reason;
#endif
}
} // namespace mkw::vr
+102 -18
View File
@@ -37,6 +37,16 @@ XrPosef IdentityPose() {
return {{0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}};
}
bool IsFinitePositive(float value) noexcept {
// mkw_runtime_common uses -ffast-math, where std::isfinite may be folded
// away. Check the IEEE-754 representation before using the scale in
// rounding and integer conversion.
uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
return (bits & 0x80000000u) == 0 && (bits & 0x7fffffffu) != 0 &&
(bits & 0x7f800000u) != 0x7f800000u;
}
uint32_t ScaledDimension(uint32_t recommended, uint32_t maximum, float scale) {
const double scaled = std::round(static_cast<double>(recommended) *
static_cast<double>(scale));
@@ -89,7 +99,7 @@ bool OpenXRRuntime::Initialize(const OpenXRConfig& config) {
return Fail(XR_ERROR_VALIDATION_FAILURE, "Initialize",
"application_name must not be empty");
}
if (!std::isfinite(config.resolution_scale) || config.resolution_scale <= 0.0f) {
if (!IsFinitePositive(config.resolution_scale)) {
return Fail(XR_ERROR_VALIDATION_FAILURE, "Initialize",
"resolution_scale must be finite and greater than zero");
}
@@ -342,6 +352,10 @@ bool OpenXRRuntime::CreateSession(const void* graphics_binding) {
return Fail(XR_ERROR_INSTANCE_LOST, "CreateSession",
"the OpenXR instance is loss-pending");
}
if (m_session_loss_pending) {
return Fail(XR_ERROR_SESSION_LOST, "CreateSession",
"session loss requires a full OpenXR reinitialization");
}
if (HasSession()) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "CreateSession",
"a session already exists");
@@ -497,20 +511,29 @@ OpenXREventStatus OpenXRRuntime::PollEvents() {
}
case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING:
m_instance_loss_pending = true;
m_session_running = false;
ResetFrameState();
Log(OpenXRLogLevel::Warning,
"OpenXR runtime reported instance loss pending");
break;
case XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING: {
const auto& space_event =
*reinterpret_cast<const XrEventDataReferenceSpaceChangePending*>(&event);
if (space_event.session == m_session) {
++m_reference_space_change.serial;
// This slot is consumed to invalidate transforms located in the
// application space. Events for VIEW or another supported type
// must not overwrite a pending LOCAL/STAGE change.
if (space_event.session == m_session &&
space_event.referenceSpaceType == m_app_space_type) {
if (++m_reference_space_change.serial == 0) {
++m_reference_space_change.serial;
}
m_reference_space_change.type = space_event.referenceSpaceType;
m_reference_space_change.change_time = space_event.changeTime;
m_reference_space_change.pose_in_previous_space_valid =
space_event.poseValid == XR_TRUE;
m_reference_space_change.pose_in_previous_space =
space_event.poseInPreviousSpace;
m_pending_app_space_changes.push_back(m_reference_space_change);
}
break;
}
@@ -546,24 +569,35 @@ bool OpenXRRuntime::HandleSessionStateChanged(
if (!Check(xrBeginSession(m_session, &begin_info), "xrBeginSession")) {
return false;
}
ResetFrameState();
m_session_running = true;
return true;
}
case XR_SESSION_STATE_STOPPING:
if (m_session_running) {
if (!Check(xrEndSession(m_session), "xrEndSession")) {
return false;
}
m_session_running = false;
if (++m_session_run_serial == 0) {
++m_session_run_serial;
}
return true;
}
case XR_SESSION_STATE_STOPPING: {
if (m_session_running) {
const XrResult result = xrEndSession(m_session);
// The OpenXR session is no longer running after any xrEndSession
// call, including one that returns an error.
m_session_running = false;
ResetFrameState();
if (!Check(result, "xrEndSession")) {
return false;
}
}
return true;
}
case XR_SESSION_STATE_EXITING:
m_exit_requested = true;
m_session_running = false;
ResetFrameState();
return true;
case XR_SESSION_STATE_LOSS_PENDING:
m_instance_loss_pending = true;
m_session_loss_pending = true;
m_session_running = false;
ResetFrameState();
return true;
default:
return true;
@@ -578,6 +612,24 @@ bool OpenXRRuntime::RequestExitSession() {
return Check(xrRequestExitSession(m_session), "xrRequestExitSession");
}
void OpenXRRuntime::ObserveResult(XrResult result) noexcept {
if (result == XR_SESSION_LOSS_PENDING) {
if (!m_session_loss_pending) {
Log(OpenXRLogLevel::Warning,
"OpenXR reported XR_SESSION_LOSS_PENDING");
}
m_session_loss_pending = true;
} else if (result == XR_ERROR_SESSION_LOST) {
m_session_loss_pending = true;
m_session_running = false;
ResetFrameState();
} else if (result == XR_ERROR_INSTANCE_LOST) {
m_instance_loss_pending = true;
m_session_running = false;
ResetFrameState();
}
}
OpenXRFrameStatus OpenXRRuntime::WaitFrame(OpenXRFrame& frame) {
ClearError();
if (ShouldExit()) {
@@ -731,9 +783,29 @@ bool OpenXRRuntime::ResetAppSpace(const XrPosef& pose_in_reference_space) {
xrDestroySpace(m_app_space);
}
m_app_space = replacement;
if (++m_reference_space_change.serial == 0) {
++m_reference_space_change.serial;
}
m_reference_space_change.type = m_app_space_type;
m_reference_space_change.change_time = 0;
m_reference_space_change.pose_in_previous_space_valid = false;
m_reference_space_change.pose_in_previous_space = IdentityPose();
m_pending_app_space_changes.push_back(m_reference_space_change);
return true;
}
bool OpenXRRuntime::ConsumeAppSpaceChangesThrough(XrTime display_time) {
bool consumed = false;
std::erase_if(m_pending_app_space_changes,
[&](const OpenXRReferenceSpaceChange& change) {
const bool due = change.change_time == 0 ||
display_time >= change.change_time;
consumed = consumed || due;
return due;
});
return consumed;
}
void OpenXRRuntime::DestroySession() {
if (!HasSession()) {
ResetSessionState();
@@ -766,9 +838,12 @@ void OpenXRRuntime::DestroySession() {
const auto timeout =
std::chrono::milliseconds(m_config.shutdown_timeout_ms);
const auto deadline = std::chrono::steady_clock::now() + timeout;
bool event_error = false;
while (m_session_running &&
std::chrono::steady_clock::now() < deadline) {
if (PollEvents() == OpenXREventStatus::Error) {
const OpenXREventStatus status = PollEvents();
if (status != OpenXREventStatus::Continue) {
event_error = status == OpenXREventStatus::Error;
break;
}
if (m_session_running) {
@@ -776,8 +851,9 @@ void OpenXRRuntime::DestroySession() {
}
}
if (m_session_running) {
Log(OpenXRLogLevel::Warning,
"OpenXR runtime did not finish session exit before timeout");
Log(OpenXRLogLevel::Warning, event_error
? "OpenXR event processing failed during session teardown"
: "OpenXR runtime did not finish session exit before timeout");
}
}
}
@@ -819,18 +895,19 @@ void OpenXRRuntime::ResetSessionState() {
m_session_running = false;
m_exit_requested = false;
m_shutting_down_session = false;
m_frame_phase = FramePhase::Idle;
m_active_frame_serial = 0;
m_active_frame_display_time = 0;
ResetFrameState();
m_supported_reference_spaces.clear();
m_swapchain_formats.clear();
m_reference_space_change = {};
m_pending_app_space_changes.clear();
}
void OpenXRRuntime::ResetInstanceState() {
m_instance = XR_NULL_HANDLE;
m_system_id = XR_NULL_SYSTEM_ID;
m_session_loss_pending = false;
m_instance_loss_pending = false;
m_session_run_serial = 0;
m_runtime_info = {};
m_view_configuration = {};
m_available_extensions.clear();
@@ -847,7 +924,14 @@ bool OpenXRRuntime::IsFrameTokenCurrent(
frame.serial == m_active_frame_serial;
}
void OpenXRRuntime::ResetFrameState() {
m_frame_phase = FramePhase::Idle;
m_active_frame_serial = 0;
m_active_frame_display_time = 0;
}
bool OpenXRRuntime::Check(XrResult result, std::string_view operation) {
ObserveResult(result);
if (XR_SUCCEEDED(result)) {
return true;
}
+630
View File
@@ -0,0 +1,630 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#if defined(MKW_ENABLE_OPENXR)
#include "vr/openxr_vulkan_backend.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <sstream>
#include <type_traits>
#include <utility>
#if __has_include(<vulkan/vulkan.h>)
#define MKW_OPENXR_VULKAN_HEADERS_AVAILABLE 1
#include <vulkan/vulkan.h>
#define XR_USE_GRAPHICS_API_VULKAN
#include <openxr/openxr_platform.h>
#else
#define MKW_OPENXR_VULKAN_HEADERS_AVAILABLE 0
#endif
namespace mkw::vr {
namespace {
constexpr std::array<OpenXRViewConfiguration, kOpenXREyeCount>
kEmptyViewConfiguration{};
constexpr std::string_view kVulkanEnable2Extension =
"XR_KHR_vulkan_enable2";
bool HasExtension(const OpenXRRuntime& runtime, std::string_view extension) {
const auto& extensions = runtime.EnabledExtensions();
return std::find(extensions.begin(), extensions.end(), extension) !=
extensions.end();
}
#if MKW_OPENXR_VULKAN_HEADERS_AVAILABLE
template <typename VulkanHandle>
VulkanHandle FromOpaqueHandle(OpenXRVulkanHandle handle) {
if constexpr (std::is_pointer_v<VulkanHandle>) {
return reinterpret_cast<VulkanHandle>(static_cast<uintptr_t>(handle));
} else {
return static_cast<VulkanHandle>(handle);
}
}
template <typename VulkanHandle>
OpenXRVulkanHandle ToOpaqueHandle(VulkanHandle handle) {
if constexpr (std::is_pointer_v<VulkanHandle>) {
return static_cast<OpenXRVulkanHandle>(reinterpret_cast<uintptr_t>(handle));
} else {
return static_cast<OpenXRVulkanHandle>(handle);
}
}
XrVersion VulkanApiVersionAsXrVersion(uint32_t version) {
return XR_MAKE_VERSION(VK_API_VERSION_MAJOR(version),
VK_API_VERSION_MINOR(version),
VK_API_VERSION_PATCH(version));
}
#endif
} // namespace
OpenXRVulkanBackend::OpenXRVulkanBackend(OpenXRLogCallback logger)
: m_logger(std::move(logger)) {}
OpenXRVulkanBackend::~OpenXRVulkanBackend() {
Shutdown();
}
OpenXRVulkanCapabilityInfo OpenXRVulkanBackend::DawnInteropCapability() {
#if !MKW_OPENXR_VULKAN_HEADERS_AVAILABLE
return {OpenXRVulkanCapability::VulkanHeadersUnavailable,
"Vulkan headers were not available when the runtime was built"};
#elif !defined(MKW_AURORA_DAWN_VULKAN_NATIVE_HANDLES)
return {
OpenXRVulkanCapability::DawnNativeHandlesUnavailable,
"the pinned Dawn package exposes VkInstance only; same-device OpenXR "
"also requires Aurora to expose Dawn's VkPhysicalDevice, VkDevice, "
"VkQueue, queue-family index, and queue synchronization",
};
#else
return {OpenXRVulkanCapability::Available, {}};
#endif
}
bool OpenXRVulkanBackend::Initialize(
OpenXRRuntime& runtime,
const OpenXRVulkanNativeContext& native_context,
const OpenXRVulkanBackendConfig& config) {
m_last_error = {};
if (IsInitialized()) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "Initialize",
"the Vulkan OpenXR backend is already initialized");
}
if (!runtime.IsInitialized()) {
return Fail(XR_ERROR_HANDLE_INVALID, "Initialize",
"OpenXRRuntime must be initialized first");
}
if (runtime.HasSession()) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "Initialize",
"OpenXRRuntime already owns a graphics session");
}
if (!HasExtension(runtime, kVulkanEnable2Extension)) {
return Fail(XR_ERROR_EXTENSION_NOT_PRESENT, "Initialize",
"XR_KHR_vulkan_enable2 was not enabled on the instance");
}
#if !MKW_OPENXR_VULKAN_HEADERS_AVAILABLE
(void)native_context;
(void)config;
return Fail(XR_ERROR_GRAPHICS_DEVICE_INVALID, "Initialize",
"this build has no Vulkan headers");
#else
m_runtime = &runtime;
m_config = config;
m_native_context = native_context;
if (!ValidateNativeContext(native_context) || !ValidateRuntimeDevice()) {
m_runtime = nullptr;
return false;
}
const XrGraphicsBindingVulkan2KHR graphics_binding{
XR_TYPE_GRAPHICS_BINDING_VULKAN2_KHR,
nullptr,
FromOpaqueHandle<VkInstance>(native_context.instance),
FromOpaqueHandle<VkPhysicalDevice>(native_context.physical_device),
FromOpaqueHandle<VkDevice>(native_context.device),
native_context.queue_family_index,
native_context.queue_index,
};
if (!runtime.CreateSession(&graphics_binding)) {
m_last_error = runtime.LastError();
m_runtime = nullptr;
return false;
}
m_owns_session = true;
if (!SelectSwapchainFormat() || !CreateSwapchains()) {
DestroySwapchains();
runtime.DestroySession();
m_owns_session = false;
m_runtime = nullptr;
return false;
}
std::ostringstream message;
message << "OpenXR Vulkan swapchains ready: format " << m_swapchain_format
<< ", left " << m_eye_swapchains[0].width << 'x'
<< m_eye_swapchains[0].height << ", right "
<< m_eye_swapchains[1].width << 'x'
<< m_eye_swapchains[1].height;
Log(OpenXRLogLevel::Info, message.str());
return true;
#endif
}
bool OpenXRVulkanBackend::ValidateNativeContext(
const OpenXRVulkanNativeContext& native_context) {
if (native_context.version != kOpenXRVulkanNativeContextVersion ||
native_context.struct_size < sizeof(OpenXRVulkanNativeContext)) {
return Fail(XR_ERROR_VALIDATION_FAILURE, "ValidateNativeContext",
"unsupported native context ABI version or size");
}
if (native_context.instance == 0 || native_context.physical_device == 0 ||
native_context.device == 0 || native_context.queue == 0 ||
native_context.api_version == 0) {
return Fail(XR_ERROR_GRAPHICS_DEVICE_INVALID, "ValidateNativeContext",
"instance, physical device, device, queue, and API version "
"must all be supplied");
}
constexpr uint32_t required_flags =
OpenXRVulkanNativeContextDawnOwnedBit |
OpenXRVulkanNativeContextQueueVerifiedBit;
if ((native_context.flags & required_flags) != required_flags) {
return Fail(XR_ERROR_GRAPHICS_DEVICE_INVALID, "ValidateNativeContext",
"native handles must be the verified Dawn device and queue");
}
if (native_context.lock_queue == nullptr ||
native_context.unlock_queue == nullptr) {
return Fail(XR_ERROR_VALIDATION_FAILURE, "ValidateNativeContext",
"paired Dawn graphics-queue lock callbacks are required");
}
return true;
}
bool OpenXRVulkanBackend::ValidateRuntimeDevice() {
#if !MKW_OPENXR_VULKAN_HEADERS_AVAILABLE
return false;
#else
PFN_xrGetVulkanGraphicsRequirements2KHR get_requirements = nullptr;
PFN_xrGetVulkanGraphicsDevice2KHR get_graphics_device = nullptr;
if (!m_runtime->LoadFunction("xrGetVulkanGraphicsRequirements2KHR",
&get_requirements) ||
!m_runtime->LoadFunction("xrGetVulkanGraphicsDevice2KHR",
&get_graphics_device)) {
m_last_error = m_runtime->LastError();
return false;
}
XrGraphicsRequirementsVulkan2KHR requirements{
XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN2_KHR};
if (!Check(get_requirements(m_runtime->Instance(), m_runtime->SystemId(),
&requirements),
"xrGetVulkanGraphicsRequirements2KHR")) {
return false;
}
const XrVersion supplied_version =
VulkanApiVersionAsXrVersion(m_native_context.api_version);
if (supplied_version < requirements.minApiVersionSupported ||
supplied_version > requirements.maxApiVersionSupported) {
std::ostringstream detail;
detail << "Dawn Vulkan version "
<< VK_API_VERSION_MAJOR(m_native_context.api_version) << '.'
<< VK_API_VERSION_MINOR(m_native_context.api_version) << '.'
<< VK_API_VERSION_PATCH(m_native_context.api_version)
<< " is outside the OpenXR runtime range "
<< XR_VERSION_MAJOR(requirements.minApiVersionSupported) << '.'
<< XR_VERSION_MINOR(requirements.minApiVersionSupported) << '-'
<< XR_VERSION_MAJOR(requirements.maxApiVersionSupported) << '.'
<< XR_VERSION_MINOR(requirements.maxApiVersionSupported);
return Fail(XR_ERROR_GRAPHICS_DEVICE_INVALID,
"xrGetVulkanGraphicsRequirements2KHR", detail.str());
}
XrVulkanGraphicsDeviceGetInfoKHR device_get_info{
XR_TYPE_VULKAN_GRAPHICS_DEVICE_GET_INFO_KHR};
device_get_info.systemId = m_runtime->SystemId();
device_get_info.vulkanInstance =
FromOpaqueHandle<VkInstance>(m_native_context.instance);
VkPhysicalDevice runtime_physical_device = VK_NULL_HANDLE;
if (!Check(get_graphics_device(m_runtime->Instance(), &device_get_info,
&runtime_physical_device),
"xrGetVulkanGraphicsDevice2KHR")) {
return false;
}
if (ToOpaqueHandle(runtime_physical_device) !=
m_native_context.physical_device) {
return Fail(XR_ERROR_GRAPHICS_DEVICE_INVALID,
"xrGetVulkanGraphicsDevice2KHR",
"OpenXR requires a different physical device than Dawn selected");
}
return true;
#endif
}
bool OpenXRVulkanBackend::SelectSwapchainFormat() {
#if !MKW_OPENXR_VULKAN_HEADERS_AVAILABLE
return false;
#else
const auto& formats = m_runtime->SwapchainFormats();
if (formats.empty()) {
return Fail(XR_ERROR_SWAPCHAIN_FORMAT_UNSUPPORTED,
"xrEnumerateSwapchainFormats",
"the runtime reported no Vulkan swapchain formats");
}
constexpr std::array<VkFormat, 6> preferred_formats{
VK_FORMAT_R8G8B8A8_SRGB,
VK_FORMAT_B8G8R8A8_SRGB,
VK_FORMAT_R8G8B8A8_UNORM,
VK_FORMAT_B8G8R8A8_UNORM,
VK_FORMAT_A2B10G10R10_UNORM_PACK32,
VK_FORMAT_R16G16B16A16_SFLOAT,
};
for (VkFormat preferred : preferred_formats) {
const int64_t candidate = static_cast<int64_t>(preferred);
if (std::find(formats.begin(), formats.end(), candidate) != formats.end()) {
m_swapchain_format = candidate;
return true;
}
}
// A caller-provided GPU bridge can still support a format outside our
// preference list, so keep the runtime's first valid choice visible.
m_swapchain_format = formats.front();
Log(OpenXRLogLevel::Warning,
"OpenXR Vulkan runtime offered no preferred RGBA/BGRA format; using "
"its first advertised format");
return true;
#endif
}
bool OpenXRVulkanBackend::CreateSwapchains() {
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
if (!CreateEyeSwapchain(eye)) {
return false;
}
}
return true;
}
bool OpenXRVulkanBackend::CreateEyeSwapchain(uint32_t eye) {
#if !MKW_OPENXR_VULKAN_HEADERS_AVAILABLE
(void)eye;
return false;
#else
const auto& view_configuration = m_runtime->ViewConfiguration();
EyeSwapchain& swapchain = m_eye_swapchains[eye];
swapchain.width = view_configuration[eye].render_width;
swapchain.height = view_configuration[eye].render_height;
XrSwapchainUsageFlags usage = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
if (m_config.require_transfer_destination) {
usage |= XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
}
const XrSwapchainCreateInfo create_info{
XR_TYPE_SWAPCHAIN_CREATE_INFO,
nullptr,
0,
usage,
m_swapchain_format,
1,
swapchain.width,
swapchain.height,
1,
1,
1,
};
if (!Check(xrCreateSwapchain(m_runtime->Session(), &create_info,
&swapchain.handle),
eye == 0 ? "xrCreateSwapchain(left)"
: "xrCreateSwapchain(right)")) {
return false;
}
uint32_t image_count = 0;
if (!Check(xrEnumerateSwapchainImages(swapchain.handle, 0, &image_count,
nullptr),
"xrEnumerateSwapchainImages(count)")) {
return false;
}
if (image_count == 0) {
return Fail(XR_ERROR_RUNTIME_FAILURE, "xrEnumerateSwapchainImages",
"the runtime returned an empty Vulkan swapchain");
}
std::vector<XrSwapchainImageVulkan2KHR> images(
image_count,
XrSwapchainImageVulkan2KHR{XR_TYPE_SWAPCHAIN_IMAGE_VULKAN2_KHR});
if (!Check(xrEnumerateSwapchainImages(
swapchain.handle, image_count, &image_count,
reinterpret_cast<XrSwapchainImageBaseHeader*>(images.data())),
"xrEnumerateSwapchainImages")) {
return false;
}
swapchain.images.reserve(image_count);
for (const XrSwapchainImageVulkan2KHR& image : images) {
swapchain.images.push_back(ToOpaqueHandle(image.image));
}
return true;
#endif
}
bool OpenXRVulkanBackend::AcquireEyeImage(uint32_t eye,
OpenXRVulkanEyeImage& image) {
if (!IsInitialized() || eye >= kOpenXREyeCount) {
return Fail(XR_ERROR_VALIDATION_FAILURE, "AcquireEyeImage",
"backend is not initialized or eye index is invalid");
}
EyeSwapchain& swapchain = m_eye_swapchains[eye];
if (swapchain.acquired && swapchain.waited) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "AcquireEyeImage",
"this eye already has an acquired image");
}
if (!swapchain.acquired) {
XrSwapchainImageAcquireInfo acquire_info{
XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
if (!LockQueue()) {
return false;
}
const XrResult acquire_result =
xrAcquireSwapchainImage(swapchain.handle, &acquire_info,
&swapchain.acquired_index);
UnlockQueue();
if (!Check(acquire_result, "xrAcquireSwapchainImage")) {
return false;
}
swapchain.acquired = true;
}
const XrSwapchainImageWaitInfo wait_info{
XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO, nullptr,
m_config.image_wait_timeout};
const XrResult wait_result = xrWaitSwapchainImage(swapchain.handle, &wait_info);
if (wait_result == XR_TIMEOUT_EXPIRED) {
// XR_TIMEOUT_EXPIRED is a positive XrResult, but the image has not
// completed its wait and therefore cannot be used or released yet.
return Fail(wait_result, "xrWaitSwapchainImage",
"the image wait timed out; retry acquisition to resume the wait");
}
if (!Check(wait_result, "xrWaitSwapchainImage")) {
// A timeout leaves the image acquired but not waited. The caller may
// retry AcquireEyeImage(), which resumes at xrWaitSwapchainImage rather
// than illegally acquiring a second image or releasing an unwaited one.
return false;
}
swapchain.waited = true;
if (swapchain.acquired_index >= swapchain.images.size()) {
ReleaseEyeImage(eye);
return Fail(XR_ERROR_RUNTIME_FAILURE, "xrAcquireSwapchainImage",
"the runtime returned an out-of-range image index");
}
image = {
eye,
swapchain.acquired_index,
swapchain.width,
swapchain.height,
m_swapchain_format,
swapchain.images[swapchain.acquired_index],
};
return true;
}
bool OpenXRVulkanBackend::ReleaseEyeImage(uint32_t eye) {
if (!IsInitialized() || eye >= kOpenXREyeCount) {
return Fail(XR_ERROR_VALIDATION_FAILURE, "ReleaseEyeImage",
"backend is not initialized or eye index is invalid");
}
EyeSwapchain& swapchain = m_eye_swapchains[eye];
if (!swapchain.acquired) {
return true;
}
if (!swapchain.waited) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "ReleaseEyeImage",
"xrWaitSwapchainImage has not completed for this image");
}
XrSwapchainImageReleaseInfo release_info{
XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
if (!LockQueue()) {
return false;
}
const XrResult release_result =
xrReleaseSwapchainImage(swapchain.handle, &release_info);
UnlockQueue();
if (!Check(release_result, "xrReleaseSwapchainImage")) {
return false;
}
swapchain.acquired = false;
swapchain.waited = false;
return true;
}
bool OpenXRVulkanBackend::SubmitProjection(const OpenXRFrame& frame) {
if (!IsInitialized()) {
return Fail(XR_ERROR_HANDLE_INVALID, "SubmitProjection",
"backend is not initialized");
}
for (const EyeSwapchain& swapchain : m_eye_swapchains) {
if (swapchain.acquired) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "SubmitProjection",
"all eye images must be released before xrEndFrame");
}
}
const bool position_valid =
(frame.view_state_flags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0;
if (!frame.should_render || !frame.views_valid || !position_valid) {
return m_runtime->EndFrameWithoutLayers(frame);
}
std::array<XrCompositionLayerProjectionView, kOpenXREyeCount> views{};
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
views[eye] = {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW};
views[eye].pose = frame.views[eye].pose;
views[eye].fov = frame.views[eye].fov;
views[eye].subImage.swapchain = m_eye_swapchains[eye].handle;
views[eye].subImage.imageRect = {
{0, 0},
{static_cast<int32_t>(m_eye_swapchains[eye].width),
static_cast<int32_t>(m_eye_swapchains[eye].height)},
};
views[eye].subImage.imageArrayIndex = 0;
}
const XrCompositionLayerProjection layer{
XR_TYPE_COMPOSITION_LAYER_PROJECTION,
nullptr,
m_config.projection_layer_flags,
m_runtime->AppSpace(),
kOpenXREyeCount,
views.data(),
};
const XrCompositionLayerBaseHeader* layers[] = {
reinterpret_cast<const XrCompositionLayerBaseHeader*>(&layer),
};
if (!m_runtime->EndFrame(frame, layers, 1)) {
m_last_error = m_runtime->LastError();
return false;
}
return true;
}
void OpenXRVulkanBackend::DestroySwapchains() {
for (EyeSwapchain& swapchain : m_eye_swapchains) {
bool may_destroy_swapchain = true;
if (swapchain.acquired && swapchain.waited &&
swapchain.handle != XR_NULL_HANDLE) {
XrSwapchainImageReleaseInfo release_info{
XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
if (LockQueue()) {
const XrResult result =
xrReleaseSwapchainImage(swapchain.handle, &release_info);
UnlockQueue();
m_runtime->ObserveResult(result);
if (XR_FAILED(result)) {
may_destroy_swapchain = false;
Log(OpenXRLogLevel::Warning,
"xrReleaseSwapchainImage failed during Vulkan shutdown");
} else {
swapchain.acquired = false;
swapchain.waited = false;
}
} else {
may_destroy_swapchain = false;
}
} else if (swapchain.acquired) {
// OpenXR does not permit releasing before a successful wait. Leave
// this child to xrDestroySession instead of violating the image
// call order with a best-effort release or destroy.
may_destroy_swapchain = false;
Log(OpenXRLogLevel::Warning,
"Vulkan swapchain still has an unwaited image; deferring its "
"destruction to xrDestroySession");
}
if (swapchain.handle != XR_NULL_HANDLE && m_runtime != nullptr &&
m_runtime->HasSession() && may_destroy_swapchain) {
const XrResult result = xrDestroySwapchain(swapchain.handle);
m_runtime->ObserveResult(result);
if (XR_FAILED(result)) {
Log(OpenXRLogLevel::Warning,
"xrDestroySwapchain failed during Vulkan shutdown");
}
}
swapchain = {};
}
m_swapchain_format = 0;
}
void OpenXRVulkanBackend::Shutdown() {
if (m_runtime == nullptr) {
return;
}
DestroySwapchains();
if (m_owns_session && m_runtime->HasSession()) {
m_runtime->DestroySession();
}
m_owns_session = false;
m_runtime = nullptr;
m_native_context = {};
}
const std::array<OpenXRViewConfiguration, kOpenXREyeCount>&
OpenXRVulkanBackend::ViewConfiguration() const {
return m_runtime != nullptr ? m_runtime->ViewConfiguration()
: kEmptyViewConfiguration;
}
bool OpenXRVulkanBackend::LockQueue() {
if (m_native_context.lock_queue == nullptr) {
return Fail(XR_ERROR_VALIDATION_FAILURE, "LockQueue",
"no Vulkan queue lock callback is installed");
}
if (!m_native_context.lock_queue(m_native_context.queue_userdata)) {
return Fail(XR_ERROR_RUNTIME_FAILURE, "LockQueue",
"Aurora refused the Vulkan graphics queue lock");
}
return true;
}
void OpenXRVulkanBackend::UnlockQueue() {
m_native_context.unlock_queue(m_native_context.queue_userdata);
}
bool OpenXRVulkanBackend::Check(XrResult result, std::string_view operation) {
if (m_runtime != nullptr) {
m_runtime->ObserveResult(result);
}
if (XR_SUCCEEDED(result)) {
return true;
}
return Fail(result, operation, {});
}
bool OpenXRVulkanBackend::Fail(XrResult result, std::string_view operation,
std::string_view detail) {
m_last_error.result = result;
m_last_error.operation.assign(operation);
m_last_error.message = ResultString(result);
if (!detail.empty()) {
m_last_error.message.append(": ");
m_last_error.message.append(detail);
}
std::string message = m_last_error.operation;
message.append(" failed: ");
message.append(m_last_error.message);
Log(OpenXRLogLevel::Error, message);
return false;
}
void OpenXRVulkanBackend::Log(OpenXRLogLevel level,
std::string_view message) const noexcept {
if (!m_logger) {
return;
}
try {
m_logger(level, message);
} catch (...) {
// Diagnostics must not escape a graphics/session teardown path.
}
}
std::string OpenXRVulkanBackend::ResultString(XrResult result) const {
char buffer[XR_MAX_RESULT_STRING_SIZE]{};
if (m_runtime != nullptr && m_runtime->IsInitialized() &&
XR_SUCCEEDED(xrResultToString(m_runtime->Instance(), result, buffer))) {
return buffer;
}
return std::to_string(static_cast<int32_t>(result));
}
} // namespace mkw::vr
#endif // defined(MKW_ENABLE_OPENXR)