Initial OpenXR configuration

This commit is contained in:
iChris4 committed 2026-08-30 21:47:12 +02:00
1 parent 9a22de1079
commit 0beb7d880c
18 files changed
+1904 -3

No files matched your search

+49
View File
@@ -77,6 +77,43 @@ typedef struct AuroraEvent AuroraEvent;
typedef void (*AuroraLogCallback)(AuroraLogLevel level, const char* module, const char* message, unsigned int len);
typedef void (*AuroraImGuiInitCallback)(const AuroraWindowSize* size);
enum { AURORA_STEREO_EYE_COUNT = 2 };
/**
* One eye of a stereo frame supplied by the host application.
*
* projection is a row-major, renderer-ready 4x4 projection matrix. It uses
* the same convention as the matrix uploaded by GXSetProjection after
* Aurora's depth-range adjustment.
*
* viewFromCenter is a row-major affine 3x4 transform from the game's
* recorded center-eye view space into this eye's view space. Identity keeps
* the recorded view and is useful when the game has already applied the eye
* transform before issuing GX commands.
*/
typedef struct {
uint32_t width;
uint32_t height;
float projection[16];
float viewFromCenter[12];
} AuroraStereoEye;
/**
* Stereo data for one sealed GX frame. frameToken is opaque to Aurora and is
* forwarded unchanged to the internal stereo output sink.
*/
typedef struct {
uint64_t frameToken;
AuroraStereoEye eyes[AURORA_STEREO_EYE_COUNT];
} AuroraStereoFrame;
/**
* Called on Aurora's frame worker immediately before a GX frame is sealed.
* Return false to render that logical frame in mono only. The callback must
* be non-blocking and must not call back into Aurora.
*/
typedef bool (*AuroraStereoFrameProvider)(uint32_t logicalFrame, AuroraStereoFrame* frame, void* userdata);
typedef struct {
const char* appName;
const char* userPath;
@@ -125,6 +162,15 @@ typedef struct {
// Optional directory for the portable GX pipeline database. When null, the
// database is stored in cachePath with Dawn's machine-specific cache.
const char* pipelineCachePath;
// Enables renderer features needed by an external XR compositor. The normal
// desktop path is unchanged when false.
bool xrInterop;
// Optional OpenXR-selected D3D adapter. Supplying the runtime's LUID before
// device creation keeps Dawn and the compositor on the same physical GPU.
bool hasD3D12AdapterLuid;
uint32_t d3d12AdapterLuidLow;
int32_t d3d12AdapterLuidHigh;
} AuroraConfig;
typedef struct {
@@ -144,6 +190,9 @@ typedef void (*AuroraFrameWorkerWaitCallback)();
// Called from the producer thread at bounded intervals while Aurora waits for
// the asynchronous frame worker. The callback must not enter Aurora.
void aurora_set_frame_worker_wait_callback(AuroraFrameWorkerWaitCallback callback);
// Registering nullptr restores the ordinary mono-only render path. Replace or
// unregister a provider only while Aurora's frame worker is idle.
void aurora_set_stereo_frame_provider(AuroraStereoFrameProvider provider, void* userdata);
void aurora_wait_for_frame_worker();
bool aurora_wait_for_frame_worker_for(uint32_t timeoutMicros);
// Absolute schedule for the next sealed frame, on steady_clock: baseNanos anchors the group and
+31
View File
@@ -281,7 +281,32 @@ void shutdown();
bool begin_frame();
bool resume_frame();
void abort_frame() noexcept;
struct ReplayTarget {
wgpu::TextureView colorView;
wgpu::TextureView resolveView;
wgpu::TextureView depthView;
wgpu::Texture copySourceTexture;
wgpu::TextureView copySourceView;
wgpu::TextureView copySourceDepthView;
wgpu::Extent3D size{};
uint32_t msaaSamples = 1;
};
struct StereoReplayEye {
ReplayTarget target;
Mat4x4<float> projection;
Mat3x4<float> viewFromCenter;
};
struct StereoReplayFrame {
std::array<StereoReplayEye, AURORA_STEREO_EYE_COUNT> eyes;
};
void end_frame(const wgpu::CommandEncoder& cmd);
// Prepares eye-specific uniform copies before unmapping the staging buffer.
// Returns false without modifying the mono path when the uniform buffer has
// insufficient room for the additional copies.
bool end_frame(const wgpu::CommandEncoder& cmd, const StereoReplayFrame& stereoFrame);
void end_batch(const wgpu::CommandEncoder& cmd);
uint32_t current_frame() noexcept;
@@ -314,6 +339,12 @@ void seal_frame(SealedFrame& out) noexcept;
// so this may run concurrently with the producer's FIFO drains.
void render(SealedFrame& frame, wgpu::CommandEncoder& cmd, int32_t interpolatedFrame = -1, bool finalize = true);
// Replays only main-EFB passes into one Aurora-owned eye target. Native
// offscreen/EFB-copy passes are consumed from the mono render and are not
// mutated by stereo replay.
void render_stereo_eye(SealedFrame& frame, wgpu::CommandEncoder& cmd,
const StereoReplayFrame& stereoFrame, uint32_t eye, bool finalize = false);
// Encode the frame that is still being recorded. Only for the synchronous
// EFB-readback split path, which runs on the producer thread.
void render(wgpu::CommandEncoder& cmd, int32_t interpolatedFrame = -1, bool finalize = true);
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include <aurora/math.hpp>
namespace aurora::gfx::stereo_replay {
// Aurora stores the GX 3x4 matrices row-major. The vertex shader consumes
// them as vec4 * mat3x4, which is equivalent to the original column-vector
// affine transform. Applying an eye-space delta therefore composes delta *
// objectToCenter in the ordinary row-major notation used below.
inline Mat3x4<float> compose_affine(const Mat3x4<float>& viewFromCenter,
const Mat3x4<float>& objectToCenter) noexcept {
Mat3x4<float> out{};
for (size_t row = 0; row < 3; ++row) {
auto& dst = *(&out.m0 + row);
const auto& view = *(&viewFromCenter.m0 + row);
for (size_t column = 0; column < 3; ++column) {
dst[column] = view[0] * objectToCenter.m0[column] + view[1] * objectToCenter.m1[column] +
view[2] * objectToCenter.m2[column];
}
dst[3] = view[3] + view[0] * objectToCenter.m0[3] + view[1] * objectToCenter.m1[3] +
view[2] * objectToCenter.m2[3];
}
return out;
}
// Normals receive only the eye transform's linear part. OpenXR view deltas
// are rigid transforms, so no inverse-transpose correction is needed here.
inline Mat3x4<float> compose_normal(const Mat3x4<float>& viewFromCenter,
const Mat3x4<float>& objectToCenter) noexcept {
Mat3x4<float> out{};
for (size_t row = 0; row < 3; ++row) {
auto& dst = *(&out.m0 + row);
const auto& view = *(&viewFromCenter.m0 + row);
for (size_t column = 0; column < 3; ++column) {
dst[column] = view[0] * objectToCenter.m0[column] + view[1] * objectToCenter.m1[column] +
view[2] * objectToCenter.m2[column];
}
dst[3] = 0.0f;
}
return out;
}
} // namespace aurora::gfx::stereo_replay
+4 -2
View File
@@ -2256,9 +2256,9 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount,
.matrixTopology = matrixTopologySignature,
};
}
const bool perspective = g_gxState.projType == GX_PERSPECTIVE;
const auto uniformRanges =
build_uniform(info, vertRange.offset, ranges, drawIdentity, interpolationIdentityActive,
usedPnMtxMask);
build_uniform(info, vertRange.offset, ranges, drawIdentity, perspective, usedPnMtxMask);
s_lastDrawRecordedInterpolation = interpolationIdentityActive;
uint32_t instanceCount = 1;
@@ -2275,6 +2275,8 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount,
.idxRange = idxRange,
.uniformRange = uniformRanges.current,
.interpolatedUniformRanges = uniformRanges.interpolated,
.stereoUniformRanges = {},
.uniformReplayLayout = uniformRanges.replayLayout,
.vtxCount = vtxCount,
.indexCount = numIndices,
.instanceCount = instanceCount,
+2
View File
@@ -10,6 +10,8 @@ struct DrawData {
gfx::Range idxRange;
gfx::Range uniformRange;
std::array<gfx::Range, MaxInterpolatedFrames> interpolatedUniformRanges;
std::array<gfx::Range, AURORA_STEREO_EYE_COUNT> stereoUniformRanges;
UniformReplayLayout uniformReplayLayout;
uint32_t vtxCount;
uint32_t indexCount;
uint32_t instanceCount;
+16
View File
@@ -613,10 +613,14 @@ UniformRanges build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGrou
stage(&effectiveProj, sizeof(effectiveProj));
const size_t positionOffset = stagedSize;
uint32_t positionMatrixMask = 0;
for (u32 i = 0; i < layout.postexCount; ++i) {
const u32 slot =
layout.postexSlots[i] == UniformMatrixLayout::kCurrentPnMtx ? currentPostexSlot
: layout.postexSlots[i];
if (slot < MaxPnMtx) {
positionMatrixMask |= 1u << i;
}
stage(slot < MaxPnMtx ? &g_gxState.pnMtx[slot].pos : &g_gxState.texMtxs[slot - MaxPnMtx],
sizeof(Mat3x4<float>));
}
@@ -707,11 +711,22 @@ UniformRanges build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGrou
buf.append(texture_size_bias(tex));
}
const UniformReplayLayout replayLayout{
.projectionOffset = static_cast<uint32_t>(projectionOffset),
.positionOffset = static_cast<uint32_t>(positionOffset),
.normalOffset = static_cast<uint32_t>(normalOffset),
.positionMatrixMask = positionMatrixMask,
.positionMatrixCount = layout.postexCount,
.normalMatrixCount = layout.nrmCount,
.perspective = perspective,
};
if (!perspective || frame_interpolation_fps() == 0) {
g_gxState.stateDirty = false;
return {
.current = range,
.interpolated = {},
.replayLayout = replayLayout,
};
}
@@ -733,6 +748,7 @@ UniformRanges build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGrou
return {
.current = range,
.interpolated = interpolatedRanges,
.replayLayout = replayLayout,
};
}
} // namespace aurora::gx
+11
View File
@@ -5,9 +5,20 @@
#include "frame_interpolation.hpp"
namespace aurora::gx {
struct UniformReplayLayout {
uint32_t projectionOffset = 0;
uint32_t positionOffset = 0;
uint32_t normalOffset = 0;
uint32_t positionMatrixMask = 0;
uint8_t positionMatrixCount = 0;
uint8_t normalMatrixCount = 0;
bool perspective = false;
};
struct UniformRanges {
gfx::Range current;
std::array<gfx::Range, MaxInterpolatedFrames> interpolated;
UniformReplayLayout replayLayout;
};
ShaderInfo build_shader_info(const ShaderConfig& config) noexcept;
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "webgpu/gpu.hpp"
#include <array>
#include <cstdint>
namespace aurora::stereo {
struct EyeImage {
// Borrowed for the duration of the sink callback. A sink may encode work
// that reads the texture, but must not retain these pointers.
const wgpu::Texture* texture = nullptr;
const wgpu::TextureView* view = nullptr;
wgpu::Extent3D size{};
wgpu::TextureFormat format = wgpu::TextureFormat::Undefined;
};
struct SinkFrame {
uint64_t frameToken = 0;
uint32_t logicalFrame = 0;
std::array<EyeImage, AURORA_STEREO_EYE_COUNT> eyes{};
};
// Runs synchronously on the frame worker after both eye replays have been
// encoded and before its command buffer is submitted. Backend interop code
// can append copies/import transitions to the same encoder here.
using SinkCallback = void (*)(wgpu::CommandEncoder& encoder, const SinkFrame& frame, void* userdata) noexcept;
// Internal Aurora hook: D3D12/Vulkan OpenXR interop owns this registration.
// Registration changes must happen while the frame worker is idle.
void set_sink(SinkCallback callback, void* userdata) noexcept;
} // namespace aurora::stereo
+32 -1
View File
@@ -43,6 +43,18 @@ void clear_offscreen_cache();
namespace aurora::webgpu {
static Module Log("aurora::gpu");
#if defined(WEBGPU_DAWN) && defined(_WIN32)
// RequestAdapterOptionsLUID is a Dawn-native extension whose exported C++
// constructor cannot be called safely by this project's LLVM-MinGW consumer.
// Its wire representation is deliberately just a WebGPU chained header plus
// the Win32 LUID, so describe that C ABI directly and let RequestAdapter
// consume it through the ordinary WebGPU entry point.
struct RequestAdapterOptionsLuidWire {
wgpu::ChainedStruct chain{};
LUID adapterLuid{};
};
#endif
wgpu::Device g_device;
wgpu::Queue g_queue;
wgpu::Surface g_surface;
@@ -558,11 +570,24 @@ bool initialize(AuroraBackend auroraBackend) {
}
}
{
const wgpu::RequestAdapterOptions options{
wgpu::RequestAdapterOptions options{
.powerPreference = wgpu::PowerPreference::HighPerformance,
.backendType = backend,
.compatibleSurface = g_surface,
};
#if defined(WEBGPU_DAWN) && defined(_WIN32)
RequestAdapterOptionsLuidWire luidOptions{};
if (backend == wgpu::BackendType::D3D12 && g_config.xrInterop &&
g_config.hasD3D12AdapterLuid) {
luidOptions.chain.sType = wgpu::SType::RequestAdapterOptionsLUID;
luidOptions.adapterLuid.LowPart = g_config.d3d12AdapterLuidLow;
luidOptions.adapterLuid.HighPart = g_config.d3d12AdapterLuidHigh;
options.nextInChain = &luidOptions.chain;
Log.info("Pinning D3D12 adapter to OpenXR LUID {:08x}:{:08x}",
static_cast<uint32_t>(luidOptions.adapterLuid.HighPart),
luidOptions.adapterLuid.LowPart);
}
#endif
const auto future = g_instance.RequestAdapter(
&options, wgpu::CallbackMode::WaitAnyOnly,
[](wgpu::RequestAdapterStatus status, wgpu::Adapter adapter, wgpu::StringView message) {
@@ -653,6 +678,12 @@ bool initialize(AuroraBackend auroraBackend) {
implicitDeviceSynchronizationSupported = true;
requiredFeatures.push_back(feature);
}
#if defined(WEBGPU_DAWN) && defined(_WIN32)
if (g_config.xrInterop && g_backendType == wgpu::BackendType::D3D12 &&
feature == wgpu::FeatureName::SharedTextureMemoryD3D12Resource) {
requiredFeatures.push_back(feature);
}
#endif
}
if (!implicitDeviceSynchronizationSupported) {
Log.warn(
+47
View File
@@ -105,6 +105,13 @@ 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)
set(MKW_OPENXR_SDK_DIR "" CACHE PATH
"Optional OpenXR-SDK source tree; when empty, use a package or fetch the pinned SDK")
# Precompiled aurora + third-party package (see launcher/Prepare-NativePrebuilt.ps1).
# Empty - the default - keeps the from-source behaviour every developer build
# uses. When set, aurora-main, its extern/ tree and the vendored Crypto++ are
@@ -222,6 +229,46 @@ if(MKW_PROJECT_COMPILE_DEFINITIONS)
add_compile_definitions(${MKW_PROJECT_COMPILE_DEFINITIONS})
endif()
set(MKW_OPENXR_TARGET "")
if(MKW_ENABLE_OPENXR)
if(NOT WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(WARNING "OpenXR is not wired for this platform; disabling MKW_ENABLE_OPENXR")
set(MKW_ENABLE_OPENXR OFF)
else()
find_package(OpenXR CONFIG QUIET)
if(NOT TARGET OpenXR::openxr_loader AND NOT TARGET openxr_loader)
include(FetchContent)
set(BUILD_API_LAYERS OFF CACHE BOOL "" FORCE)
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_CONFORMANCE_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_SDK_TESTS OFF CACHE BOOL "" FORCE)
set(DYNAMIC_LOADER OFF CACHE BOOL "" FORCE)
if(MKW_OPENXR_SDK_DIR)
if(NOT EXISTS "${MKW_OPENXR_SDK_DIR}/CMakeLists.txt")
message(FATAL_ERROR
"MKW_OPENXR_SDK_DIR does not contain OpenXR-SDK: ${MKW_OPENXR_SDK_DIR}")
endif()
add_subdirectory("${MKW_OPENXR_SDK_DIR}"
"${CMAKE_BINARY_DIR}/openxr-sdk" EXCLUDE_FROM_ALL)
else()
FetchContent_Declare(mkw_openxr
URL https://github.com/KhronosGroup/OpenXR-SDK/archive/refs/tags/release-1.1.61.tar.gz
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL)
FetchContent_MakeAvailable(mkw_openxr)
endif()
endif()
if(TARGET OpenXR::openxr_loader)
set(MKW_OPENXR_TARGET OpenXR::openxr_loader)
elseif(TARGET openxr_loader)
set(MKW_OPENXR_TARGET openxr_loader)
else()
message(FATAL_ERROR "OpenXR was enabled but no loader target was provided")
endif()
add_compile_definitions(MKW_ENABLE_OPENXR=1)
endif()
endif()
# Runtime sources. CONFIGURE_DEPENDS costs one glob re-check per build (measured
# at ~55 ms across the 100+ globs this build already re-verifies for SDL), and it
# buys correctness: without it, a newly added src/*.cpp that is not referenced by
+8
View File
@@ -77,6 +77,9 @@ target_compile_definitions(mkw_runtime_common PRIVATE
target_link_libraries(mkw_runtime_common PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
target_link_libraries(mkw_runtime_common PRIVATE mkw::pugixml mkw::toml11 mkw::cryptopp)
if(MKW_ENABLE_OPENXR)
target_link_libraries(mkw_runtime_common PRIVATE ${MKW_OPENXR_TARGET})
endif()
if(WIN32)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
else()
@@ -194,6 +197,11 @@ function(mkw_configure_product target)
target_link_libraries(${target} PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
if(MKW_ENABLE_OPENXR)
# mkw_runtime_common is consumed as raw object files, so its private
# loader dependency must also be present on each final product link.
target_link_libraries(${target} PRIVATE ${MKW_OPENXR_TARGET})
endif()
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
aurora_copy_runtime_dlls(${target})
+59
View File
@@ -44,6 +44,11 @@ struct RuntimeUserConfig {
std::optional<bool> textureDumps;
std::optional<bool> showFps;
std::optional<uint32_t> disabledPostProcessingPaths;
std::optional<bool> vrEnabled;
std::optional<bool> vrRequired;
std::optional<float> vrRenderScale;
std::optional<float> vrWorldUnitsPerMeter;
std::optional<float> vrHudDistanceMeters;
std::optional<float> audioVolume;
std::optional<float> audioMusicVolume;
std::optional<float> audioSoundEffectsVolume;
@@ -272,6 +277,14 @@ inline void EnsureConfigFile() {
"# replacement would need. Both are read once, at startup.\n"
"texture_replacements = false\n"
"texture_dumps = false\n\n"
"[vr]\n"
"# OpenXR is opt-in. If required is false, startup failures fall back\n"
"# to the ordinary desktop renderer. These values are read at launch.\n"
"enabled = false\n"
"required = false\n"
"render_scale = 1.0\n"
"world_units_per_meter = 500.0\n"
"hud_distance_meters = 2.0\n\n"
"[audio]\n"
"volume = 1.0\n"
"music_volume = 1.0\n"
@@ -404,6 +417,21 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.disabledPostProcessingPaths = *value & 0x10u;
}
config.vrEnabled = FindConfigValue<bool>(document, "vr", "enabled");
config.vrRequired = FindConfigValue<bool>(document, "vr", "required");
if (auto value = FindConfigFloat(document, "vr", "render_scale");
value && *value >= 0.25f && *value <= 2.0f) {
config.vrRenderScale = *value;
}
if (auto value = FindConfigFloat(document, "vr", "world_units_per_meter");
value && *value >= 1.0f && *value <= 10000.0f) {
config.vrWorldUnitsPerMeter = *value;
}
if (auto value = FindConfigFloat(document, "vr", "hud_distance_meters");
value && *value >= 0.25f && *value <= 10.0f) {
config.vrHudDistanceMeters = *value;
}
auto readVolume = [&](std::string_view key) -> std::optional<float> {
auto value = FindConfigFloat(document, "audio", key);
return value && *value >= 0.0f && *value <= 1.0f ? value : std::nullopt;
@@ -617,6 +645,11 @@ inline bool SetDisabledPostProcessingPaths(uint32_t value) {
return WriteSetting("video", "disabled_post_processing_paths", formatted.str());
}
inline bool SetVrEnabled(bool value) {
Mutable().vrEnabled = value;
return WriteSetting("vr", "enabled", value ? "true" : "false");
}
inline bool SetControllerButton(size_t index, std::string value) {
if (index >= kControllerButtonKeys.size()) {
return false;
@@ -768,6 +801,26 @@ inline uint32_t DisabledPostProcessingPaths(uint32_t fallback = 0) {
return Get().disabledPostProcessingPaths.value_or(fallback) & 0x10u;
}
inline bool VrEnabled(bool fallback = false) {
return Get().vrEnabled.value_or(fallback);
}
inline bool VrRequired(bool fallback = false) {
return Get().vrRequired.value_or(fallback);
}
inline float VrRenderScale(float fallback = 1.0f) {
return std::clamp(Get().vrRenderScale.value_or(fallback), 0.25f, 2.0f);
}
inline float VrWorldUnitsPerMeter(float fallback = 500.0f) {
return std::clamp(Get().vrWorldUnitsPerMeter.value_or(fallback), 1.0f, 10000.0f);
}
inline float VrHudDistanceMeters(float fallback = 2.0f) {
return std::clamp(Get().vrHudDistanceMeters.value_or(fallback), 0.25f, 10.0f);
}
inline std::string GraphicsApi(std::string fallback = "auto") {
return Get().graphicsApi.value_or(std::move(fallback));
}
@@ -862,6 +915,12 @@ inline void LogLoadedConfig() {
if (config.textureDumps) {
std::cout << " texture_dumps=" << (*config.textureDumps ? "true" : "false");
}
if (config.vrEnabled) {
std::cout << " vr_enabled=" << (*config.vrEnabled ? "true" : "false");
}
if (config.vrRequired) {
std::cout << " vr_required=" << (*config.vrRequired ? "true" : "false");
}
if (config.audioVolume) {
std::cout << " audio_volume=" << *config.audioVolume;
}
+151
View File
@@ -0,0 +1,151 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
namespace mkw::vr {
// Game-facing presentation policy. The OpenXR implementation owns session and
// swapchain state; this module only decides how Mario Kart Wii content should
// be presented once the integration layer publishes game observations.
enum class VRPresentationMode : uint8_t {
Desktop,
VirtualScreen,
ImmersiveRace,
};
enum class VRSceneMode : uint8_t {
Unknown,
FrontEnd,
Race,
Replay,
Awards,
Other,
};
enum class VRProjectionKind : uint8_t {
Unknown,
Perspective,
Orthographic,
};
enum class VRDrawPass : uint8_t {
Scene,
PostProcess,
};
enum class VRDrawClass : uint8_t {
Unknown,
PerspectiveWorld,
OrthographicHud,
PostProcess,
};
enum class VRDrawRoute : uint8_t {
DesktopPassthrough,
VirtualScreen,
StereoWorld,
HeadLockedHud,
PerEyePostProcess,
Unclassified,
};
// Instrumentation capabilities are explicit so a partial binding can never
// accidentally enable immersive rendering. In particular, a camera sample by
// itself is insufficient because orthographic UI would otherwise be rendered
// with the stereo world path.
enum MkwVRBinding : uint32_t {
MkwVRBindingNone = 0,
MkwVRBindingSceneState = 1u << 0,
MkwVRBindingRaceCamera = 1u << 1,
MkwVRBindingDrawClassification = 1u << 2,
MkwVRBindingPostProcess = 1u << 3,
MkwVRBindingCulling = 1u << 4,
};
inline constexpr uint32_t kMkwVRRequiredImmersiveBindings =
MkwVRBindingSceneState | MkwVRBindingRaceCamera | MkwVRBindingDrawClassification;
struct MkwVRPolicyConfig {
bool enabled = false;
bool immersive_races = true;
float world_units_per_meter = 500.0f;
float hud_distance_meters = 2.0f;
float hud_scale = 1.0f;
};
struct MkwVRSceneObservation {
VRSceneMode mode = VRSceneMode::Unknown;
uint32_t local_player_count = 0;
uint64_t guest_frame_index = 0;
};
struct MkwVRCameraObservation {
// Mario Kart and NW4R expose affine view matrices as row-major 3x4 Mtx
// values. The integration layer is responsible for reading/converting the
// guest value; this policy never assumes a guest object layout.
std::array<float, 12> view_from_world{};
uint32_t guest_camera_address = 0;
uint64_t guest_frame_index = 0;
bool valid = false;
};
struct MkwVRDrawObservation {
VRProjectionKind projection = VRProjectionKind::Unknown;
VRDrawPass pass = VRDrawPass::Scene;
};
struct MkwVRPolicySnapshot {
VRPresentationMode presentation = VRPresentationMode::Desktop;
MkwVRPolicyConfig config{};
MkwVRSceneObservation scene{};
MkwVRCameraObservation camera{};
uint32_t available_bindings = MkwVRBindingNone;
bool session_active = false;
};
// All policy functions are thread-safe. Publishing functions are intended for
// translated-game instrumentation on the guest thread; the renderer may take a
// snapshot without retaining references to mutable policy state.
void MkwVRPolicyReset() noexcept;
void MkwVRPolicyConfigure(const MkwVRPolicyConfig& config) noexcept;
void MkwVRPolicySetSessionActive(bool active) noexcept;
void MkwVRPolicySetAvailableBindings(uint32_t bindings) noexcept;
void MkwVRPolicyPublishScene(const MkwVRSceneObservation& scene) noexcept;
void MkwVRPolicyPublishRaceCamera(const MkwVRCameraObservation& camera) noexcept;
void MkwVRPolicyInvalidateRaceCamera() noexcept;
MkwVRPolicySnapshot MkwVRPolicyGetSnapshot() noexcept;
// This classifier is deliberately structural rather than heuristic: future
// hooks report whether the active projection is perspective/orthographic and
// whether execution is inside a post-processing boundary. No matrix-value or
// guest-address guessing occurs here.
VRDrawClass MkwVRPolicyClassifyDraw(const MkwVRDrawObservation& draw) noexcept;
VRDrawRoute MkwVRPolicyRouteDraw(VRDrawClass draw_class,
const MkwVRPolicySnapshot& snapshot) noexcept;
enum class MkwVRHookCapability : uint8_t {
SceneState,
RaceCamera,
DrawClassification,
PostProcess,
Culling,
};
// Validated PAL RMCP01 symbols from projects/mkwii/MAP.txt. These are future
// instrumentation candidates, not registered native replacements: the current
// PPC_NATIVE_OVERRIDE mechanism replaces a translated function outright and
// cannot safely observe it before/after its original body.
struct MkwVRHookPoint {
uint32_t address;
const char* symbol;
MkwVRHookCapability capability;
const char* purpose;
};
const MkwVRHookPoint* MkwVRPolicyHookPoints(std::size_t* count) noexcept;
} // namespace mkw::vr
+51
View File
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <openxr/openxr.h>
namespace mkw::vr {
// Kept independent from RuntimeUserConfig so the OpenXR core can be initialized
// before Aurora chooses a graphics adapter. The application-facing TOML values
// should be translated into this structure by the runtime integration layer.
struct OpenXRConfig {
std::string application_name = "WiiCompiled";
uint32_t application_version = 1;
std::string engine_name = "Aurora";
uint32_t engine_version = 1;
// OpenXR 1.0 is sufficient for the lifecycle implemented by this class and
// remains compatible with desktop runtimes that do not advertise 1.1 yet.
XrVersion api_version = XR_API_VERSION_1_0;
XrFormFactor form_factor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
XrViewConfigurationType view_configuration =
XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
// LOCAL is the natural default for a seated racing game. STAGE can be
// requested by the integration layer and falls back to LOCAL when absent.
XrReferenceSpaceType reference_space = XR_REFERENCE_SPACE_TYPE_LOCAL;
XrEnvironmentBlendMode preferred_blend_mode =
XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
// Applied to the runtime-recommended dimensions and clamped to the
// runtime-advertised maximum for each view.
float resolution_scale = 1.0f;
// The graphics backend must put its binding extension in required_extensions
// (for example XR_KHR_D3D12_enable or XR_KHR_vulkan_enable2). Optional
// extensions/layers are enabled only when the active runtime advertises them.
std::vector<std::string> required_extensions;
std::vector<std::string> optional_extensions;
std::vector<std::string> required_api_layers;
std::vector<std::string> optional_api_layers;
// Destruction never waits indefinitely for a runtime to acknowledge an exit.
uint32_t shutdown_timeout_ms = 500;
};
} // namespace mkw::vr
+260
View File
@@ -0,0 +1,260 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "vr/openxr_config.h"
#include <array>
#include <cstdint>
#include <functional>
#include <string>
#include <string_view>
#include <type_traits>
#include <vector>
namespace mkw::vr {
inline constexpr uint32_t kOpenXREyeCount = 2;
enum class OpenXRLogLevel {
Info,
Warning,
Error,
};
using OpenXRLogCallback =
std::function<void(OpenXRLogLevel level, std::string_view message)>;
struct OpenXRError {
XrResult result = XR_SUCCESS;
std::string operation;
std::string message;
explicit operator bool() const {
return XR_FAILED(result) || !message.empty();
}
};
struct OpenXRViewConfiguration {
XrViewConfigurationView properties{XR_TYPE_VIEW_CONFIGURATION_VIEW};
uint32_t render_width = 0;
uint32_t render_height = 0;
};
struct OpenXRRuntimeInfo {
std::string runtime_name;
XrVersion runtime_version = 0;
std::string system_name;
uint32_t vendor_id = 0;
uint32_t max_layer_count = 0;
bool supports_orientation_tracking = false;
bool supports_position_tracking = false;
};
struct OpenXRReferenceSpaceChange {
uint64_t serial = 0;
XrReferenceSpaceType type = XR_REFERENCE_SPACE_TYPE_LOCAL;
XrTime change_time = 0;
bool pose_in_previous_space_valid = false;
XrPosef pose_in_previous_space{{0.0f, 0.0f, 0.0f, 1.0f},
{0.0f, 0.0f, 0.0f}};
};
// A frame token is produced by WaitFrame and must be passed back to the other
// frame functions. This prevents an old pose/timestamp from being paired with a
// newer compositor frame.
struct OpenXRFrame {
uint64_t serial = 0;
XrTime predicted_display_time = 0;
XrDuration predicted_display_period = 0;
bool should_render = false;
bool views_valid = false;
XrViewStateFlags view_state_flags = 0;
std::array<XrView, kOpenXREyeCount> views{};
};
enum class OpenXREventStatus {
Continue,
ExitRequested,
Error,
};
enum class OpenXRFrameStatus {
Ready,
SessionNotRunning,
ExitRequested,
Error,
};
// Owns the backend-neutral OpenXR object graph and session state machine.
//
// A graphics backend performs its requirements/device work after Initialize(),
// then passes its XrGraphicsBinding* structure to CreateSession(). Swapchains are
// intentionally not owned here: D3D12 and Vulkan need different image wrapping
// and synchronization strategies.
//
// OpenXRRuntime is not internally synchronized. PollEvents and all frame calls
// must be serialized by one XR-owner thread. Read-only handles may be handed to
// the graphics integration only while the corresponding object is alive.
class OpenXRRuntime final {
public:
explicit OpenXRRuntime(OpenXRLogCallback logger = {});
~OpenXRRuntime();
OpenXRRuntime(const OpenXRRuntime&) = delete;
OpenXRRuntime& operator=(const OpenXRRuntime&) = delete;
OpenXRRuntime(OpenXRRuntime&&) = delete;
OpenXRRuntime& operator=(OpenXRRuntime&&) = delete;
// Creates the instance, resolves the HMD system, and enumerates the stereo
// view configuration. Returns false without terminating the application;
// the caller should continue in non-VR mode.
bool Initialize(const OpenXRConfig& config);
// graphics_binding is the address of an XrGraphicsBinding* structure and is
// forwarded through XrSessionCreateInfo::next. It must remain valid only for
// the duration of this call.
bool CreateSession(const void* graphics_binding);
// Loads an extension entry point from this instance. Backends use this to
// query graphics requirements before Aurora creates/selects its device.
bool GetInstanceProcAddress(const char* name, PFN_xrVoidFunction* function);
template <typename Function>
bool LoadFunction(const char* name, Function* function) {
static_assert(std::is_pointer_v<Function>,
"OpenXR PFN typedefs must be pointer types");
return GetInstanceProcAddress(
name, reinterpret_cast<PFN_xrVoidFunction*>(function));
}
// Requests an orderly exit when running, processes STOPPING for a bounded
// period, then releases spaces and the session. The instance/system remain
// available so a backend session can be recreated.
void DestroySession();
// Releases every owned object. Safe to call repeatedly.
void Shutdown();
// Polls every currently queued event and owns xrBeginSession/xrEndSession
// transitions for READY/STOPPING. EXITING and LOSS_PENDING are surfaced to
// the application rather than terminating it here.
OpenXREventStatus PollEvents();
bool RequestExitSession();
// 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.
OpenXRFrameStatus WaitFrame(OpenXRFrame& frame);
bool BeginFrame(const OpenXRFrame& frame);
bool LocateViews(OpenXRFrame& frame);
bool EndFrame(
const OpenXRFrame& frame,
const XrCompositionLayerBaseHeader* const* layers,
uint32_t layer_count);
bool EndFrame(
const OpenXRFrame& frame,
const std::vector<const XrCompositionLayerBaseHeader*>& layers);
bool EndFrameWithoutLayers(const OpenXRFrame& frame);
// Recreates the application reference space with a caller-provided offset.
// This is the application-side recenter primitive; call only between frames.
bool ResetAppSpace(const XrPosef& pose_in_reference_space);
bool IsInitialized() const { return m_instance != XR_NULL_HANDLE; }
bool HasSession() const { return m_session != XR_NULL_HANDLE; }
bool IsSessionRunning() const { return m_session_running; }
bool IsSessionFocused() const {
return m_session_state == XR_SESSION_STATE_FOCUSED;
}
bool IsSessionVisible() const {
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; }
XrInstance Instance() const { return m_instance; }
XrSystemId SystemId() const { return m_system_id; }
XrSession Session() const { return m_session; }
XrSpace AppSpace() const { return m_app_space; }
XrSpace ViewSpace() const { return m_view_space; }
XrSessionState SessionState() const { return m_session_state; }
XrReferenceSpaceType AppSpaceType() const { return m_app_space_type; }
XrEnvironmentBlendMode EnvironmentBlendMode() const { return m_blend_mode; }
const OpenXRConfig& Config() const { return m_config; }
const OpenXRRuntimeInfo& RuntimeInfo() const { return m_runtime_info; }
const std::array<OpenXRViewConfiguration, kOpenXREyeCount>& ViewConfiguration() const {
return m_view_configuration;
}
const std::vector<std::string>& EnabledExtensions() const {
return m_enabled_extensions;
}
const std::vector<int64_t>& SwapchainFormats() const { return m_swapchain_formats; }
const OpenXRReferenceSpaceChange& LastReferenceSpaceChange() const {
return m_reference_space_change;
}
const OpenXRError& LastError() const { return m_last_error; }
private:
enum class FramePhase {
Idle,
Waited,
Begun,
};
bool EnumerateInstanceCapabilities();
bool CreateInstance();
bool InitializeSystem();
bool EnumerateViewConfiguration();
bool SelectEnvironmentBlendMode();
bool CreateReferenceSpaces();
bool EnumerateSwapchainFormats();
bool HandleSessionStateChanged(const XrEventDataSessionStateChanged& event);
bool IsFrameTokenCurrent(const OpenXRFrame& frame, FramePhase expected) const;
void DestroyReferenceSpaces();
void ResetSessionState();
void ResetInstanceState();
bool Check(XrResult result, std::string_view operation);
bool Fail(XrResult result, std::string_view operation, std::string_view detail);
void ClearError();
void Log(OpenXRLogLevel level, std::string_view message) const noexcept;
std::string ResultString(XrResult result) const;
OpenXRLogCallback m_logger;
OpenXRConfig m_config;
OpenXRError m_last_error;
XrInstance m_instance = XR_NULL_HANDLE;
XrSystemId m_system_id = XR_NULL_SYSTEM_ID;
XrSession m_session = XR_NULL_HANDLE;
XrSpace m_app_space = XR_NULL_HANDLE;
XrSpace m_view_space = XR_NULL_HANDLE;
XrSessionState m_session_state = XR_SESSION_STATE_UNKNOWN;
XrReferenceSpaceType m_app_space_type = XR_REFERENCE_SPACE_TYPE_LOCAL;
XrEnvironmentBlendMode m_blend_mode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
bool m_session_running = false;
bool m_exit_requested = false;
bool m_instance_loss_pending = false;
bool m_shutting_down_session = false;
FramePhase m_frame_phase = FramePhase::Idle;
uint64_t m_next_frame_serial = 1;
uint64_t m_active_frame_serial = 0;
XrTime m_active_frame_display_time = 0;
OpenXRRuntimeInfo m_runtime_info;
std::array<OpenXRViewConfiguration, kOpenXREyeCount> m_view_configuration{};
std::vector<std::string> m_available_extensions;
std::vector<std::string> m_available_api_layers;
std::vector<std::string> m_enabled_extensions;
std::vector<std::string> m_enabled_api_layers;
std::vector<XrReferenceSpaceType> m_supported_reference_spaces;
std::vector<XrEnvironmentBlendMode> m_supported_blend_modes;
std::vector<int64_t> m_swapchain_formats;
OpenXRReferenceSpaceChange m_reference_space_change;
};
} // namespace mkw::vr
+5
View File
@@ -98,6 +98,7 @@ int g_displayMode = [] {
bool g_skipUnreadyPipelines = RuntimeConfigFile::SkipUnreadyPipelines(true);
bool g_disableCopyFilter = RuntimeConfigFile::DisableCopyFilter(true);
bool g_showFps = RuntimeConfigFile::ShowFps(true);
bool g_vrEnabled = RuntimeConfigFile::VrEnabled(false);
uint32_t g_disabledPostProcessingPaths = RuntimeConfigFile::DisabledPostProcessingPaths(0);
std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] {
std::array<int32_t, PAD_MAX_CONTROLLERS> indices{};
@@ -645,6 +646,10 @@ void DrawGraphicsSettings() {
}
ImGui::Separator();
ImGui::Text("Graphics API: %s", GraphicsApiDisplayName());
if (ImGui::Checkbox("Enable OpenXR VR", &g_vrEnabled)) {
RuntimeConfigFile::SetVrEnabled(g_vrEnabled);
}
ImGui::TextDisabled("OpenXR mode changes take effect after restarting the game.");
}
void DrawFpsOverlay() {
+211
View File
@@ -0,0 +1,211 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "vr/mkw_vr_policy.h"
#include <algorithm>
#include <cmath>
#include <mutex>
namespace mkw::vr {
namespace {
constexpr MkwVRPolicyConfig kDefaultConfig{};
struct PolicyState {
MkwVRPolicyConfig config = kDefaultConfig;
MkwVRSceneObservation scene{};
MkwVRCameraObservation camera{};
uint32_t available_bindings = MkwVRBindingNone;
bool session_active = false;
};
std::mutex g_policy_mutex;
PolicyState g_policy;
bool IsFinitePositive(float value) noexcept {
return std::isfinite(value) && value > 0.0f;
}
MkwVRPolicyConfig SanitizeConfig(const MkwVRPolicyConfig& config) noexcept {
MkwVRPolicyConfig sanitized = config;
if (!IsFinitePositive(sanitized.world_units_per_meter)) {
sanitized.world_units_per_meter = kDefaultConfig.world_units_per_meter;
}
if (!IsFinitePositive(sanitized.hud_distance_meters)) {
sanitized.hud_distance_meters = kDefaultConfig.hud_distance_meters;
}
if (!IsFinitePositive(sanitized.hud_scale)) {
sanitized.hud_scale = kDefaultConfig.hud_scale;
}
return sanitized;
}
bool IsFiniteCamera(const MkwVRCameraObservation& camera) noexcept {
return camera.valid &&
std::all_of(camera.view_from_world.begin(), camera.view_from_world.end(),
[](float value) { return std::isfinite(value); });
}
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.
if ((state.available_bindings & kMkwVRRequiredImmersiveBindings) !=
kMkwVRRequiredImmersiveBindings ||
!state.config.immersive_races || state.scene.mode != VRSceneMode::Race ||
state.scene.local_player_count != 1 || !IsFiniteCamera(state.camera)) {
return VRPresentationMode::VirtualScreen;
}
return VRPresentationMode::ImmersiveRace;
}
constexpr MkwVRHookPoint kHookPoints[] = {
{0x80562B34u, "ScnMgr::UpdateCameras", MkwVRHookCapability::SceneState,
"Observe the active scene camera update boundary."},
{0x80562BF0u, "GameCamera::GetViewMatrix", MkwVRHookCapability::RaceCamera,
"Observe the common camera view matrix consumed by ScnMgr."},
{0x805B2110u, "ScnMgrRace::UpdateCameras", MkwVRHookCapability::SceneState,
"Identify a race camera update without relying on a scene object layout."},
{0x805B1CD8u, "ScnMgrRace::Draw", MkwVRHookCapability::DrawClassification,
"Bracket race-scene drawing for perspective-world classification."},
{0x805A21D0u, "RaceCamera::Update", MkwVRHookCapability::RaceCamera,
"Observe the stable post-update race camera for the current guest frame."},
{0x805A6C58u, "RaceCamera::GetViewMtx", MkwVRHookCapability::RaceCamera,
"Copy the returned 3x4 view matrix through a future translated observer."},
{0x805A906Cu, "RaceCameraMgr::ApplyShaking", MkwVRHookCapability::RaceCamera,
"Separate game camera shake from headset motion when a comfort policy is added."},
{0x80565DA0u, "GameScreen::SetAndLoadOrthoProj",
MkwVRHookCapability::DrawClassification,
"Mark orthographic GameScreen work as HUD or flat-screen content."},
{0x80566020u, "GameScreen::SetAndLoadProjection",
MkwVRHookCapability::DrawClassification,
"Observe GameScreen projection changes used by race and menu UI."},
{0x805661E8u, "GameScreen::SetProjection", MkwVRHookCapability::DrawClassification,
"Observe projection setup without assuming GameScreen member offsets."},
{0x800640D0u, "nw4r::g3d::G3DState::SetCameraProjMtx",
MkwVRHookCapability::DrawClassification,
"Provide a renderer-level perspective/orthographic projection boundary."},
{0x8006AA80u, "nw4r::g3d::Camera::GXSetProjection",
MkwVRHookCapability::DrawClassification,
"Observe the final NW4R camera projection submitted to GX."},
{0x8054F41Cu, "GameScreenEffectsMgr::Draw", MkwVRHookCapability::PostProcess,
"Bracket screen effects that must be evaluated per eye or composed flat."},
{0x8054F7A4u, "GameScreenEffectsMgr::DrawCourseFilterEffects",
MkwVRHookCapability::PostProcess,
"Classify full-screen course filters as post-processing."},
{0x8054F8E0u, "GameScreenEffectsMgr::CopyEFBToLensFlareTextures",
MkwVRHookCapability::PostProcess,
"Track EFB-dependent lens-flare capture separately from world geometry."},
{0x802278D0u, "EGG::Frustum::CalcMtxPerspective", MkwVRHookCapability::Culling,
"Future culling-frustum expansion point for head movement beyond the base camera."},
{0x80228180u, "EGG::Frustum::CopyToG3D", MkwVRHookCapability::Culling,
"Observe the frustum handed to NW4R without guessing EGG::Frustum fields."},
};
} // namespace
void MkwVRPolicyReset() noexcept {
std::lock_guard<std::mutex> lock(g_policy_mutex);
g_policy = PolicyState{};
}
void MkwVRPolicyConfigure(const MkwVRPolicyConfig& config) noexcept {
std::lock_guard<std::mutex> lock(g_policy_mutex);
g_policy.config = SanitizeConfig(config);
}
void MkwVRPolicySetSessionActive(bool active) noexcept {
std::lock_guard<std::mutex> lock(g_policy_mutex);
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;
}
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;
}
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);
}
void MkwVRPolicyInvalidateRaceCamera() noexcept {
std::lock_guard<std::mutex> lock(g_policy_mutex);
g_policy.camera.valid = false;
}
MkwVRPolicySnapshot MkwVRPolicyGetSnapshot() noexcept {
std::lock_guard<std::mutex> lock(g_policy_mutex);
MkwVRPolicySnapshot snapshot;
snapshot.presentation = SelectPresentation(g_policy);
snapshot.config = g_policy.config;
snapshot.scene = g_policy.scene;
snapshot.camera = g_policy.camera;
snapshot.available_bindings = g_policy.available_bindings;
snapshot.session_active = g_policy.session_active;
return snapshot;
}
VRDrawClass MkwVRPolicyClassifyDraw(const MkwVRDrawObservation& draw) noexcept {
if (draw.pass == VRDrawPass::PostProcess) {
return VRDrawClass::PostProcess;
}
switch (draw.projection) {
case VRProjectionKind::Perspective:
return VRDrawClass::PerspectiveWorld;
case VRProjectionKind::Orthographic:
return VRDrawClass::OrthographicHud;
case VRProjectionKind::Unknown:
return VRDrawClass::Unknown;
}
return VRDrawClass::Unknown;
}
VRDrawRoute MkwVRPolicyRouteDraw(VRDrawClass draw_class,
const MkwVRPolicySnapshot& snapshot) noexcept {
switch (snapshot.presentation) {
case VRPresentationMode::Desktop:
return VRDrawRoute::DesktopPassthrough;
case VRPresentationMode::VirtualScreen:
return VRDrawRoute::VirtualScreen;
case VRPresentationMode::ImmersiveRace:
break;
}
switch (draw_class) {
case VRDrawClass::PerspectiveWorld:
return VRDrawRoute::StereoWorld;
case VRDrawClass::OrthographicHud:
return VRDrawRoute::HeadLockedHud;
case VRDrawClass::PostProcess:
return VRDrawRoute::PerEyePostProcess;
case VRDrawClass::Unknown:
return VRDrawRoute::Unclassified;
}
return VRDrawRoute::Unclassified;
}
const MkwVRHookPoint* MkwVRPolicyHookPoints(std::size_t* count) noexcept {
if (count != nullptr) {
*count = sizeof(kHookPoints) / sizeof(kHookPoints[0]);
}
return kHookPoints;
}
} // namespace mkw::vr
+889
View File
@@ -0,0 +1,889 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "vr/openxr_runtime.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstring>
#include <limits>
#include <sstream>
#include <thread>
#include <utility>
namespace mkw::vr {
namespace {
template <typename T>
bool Contains(const std::vector<T>& values, const T& value) {
return std::find(values.begin(), values.end(), value) != values.end();
}
void CopyOpenXRName(char* destination, size_t destination_size, std::string_view source) {
if (destination_size == 0) {
return;
}
const size_t length = std::min(destination_size - 1, source.size());
std::memcpy(destination, source.data(), length);
destination[length] = '\0';
}
XrPosef IdentityPose() {
return {{0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}};
}
uint32_t ScaledDimension(uint32_t recommended, uint32_t maximum, float scale) {
const double scaled = std::round(static_cast<double>(recommended) *
static_cast<double>(scale));
const double clamped = std::clamp(
scaled, 1.0, static_cast<double>(std::max(maximum, 1u)));
return static_cast<uint32_t>(clamped);
}
const char* SessionStateName(XrSessionState state) {
switch (state) {
case XR_SESSION_STATE_UNKNOWN:
return "UNKNOWN";
case XR_SESSION_STATE_IDLE:
return "IDLE";
case XR_SESSION_STATE_READY:
return "READY";
case XR_SESSION_STATE_SYNCHRONIZED:
return "SYNCHRONIZED";
case XR_SESSION_STATE_VISIBLE:
return "VISIBLE";
case XR_SESSION_STATE_FOCUSED:
return "FOCUSED";
case XR_SESSION_STATE_STOPPING:
return "STOPPING";
case XR_SESSION_STATE_LOSS_PENDING:
return "LOSS_PENDING";
case XR_SESSION_STATE_EXITING:
return "EXITING";
default:
return "INVALID";
}
}
} // namespace
OpenXRRuntime::OpenXRRuntime(OpenXRLogCallback logger)
: m_logger(std::move(logger)) {}
OpenXRRuntime::~OpenXRRuntime() {
Shutdown();
}
bool OpenXRRuntime::Initialize(const OpenXRConfig& config) {
ClearError();
if (IsInitialized()) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "Initialize",
"OpenXR is already initialized");
}
if (config.application_name.empty()) {
return Fail(XR_ERROR_VALIDATION_FAILURE, "Initialize",
"application_name must not be empty");
}
if (!std::isfinite(config.resolution_scale) || config.resolution_scale <= 0.0f) {
return Fail(XR_ERROR_VALIDATION_FAILURE, "Initialize",
"resolution_scale must be finite and greater than zero");
}
m_config = config;
if (!EnumerateInstanceCapabilities() || !CreateInstance() ||
!InitializeSystem() || !EnumerateViewConfiguration() ||
!SelectEnvironmentBlendMode()) {
if (m_instance != XR_NULL_HANDLE) {
xrDestroyInstance(m_instance);
}
ResetInstanceState();
return false;
}
std::ostringstream message;
message << "OpenXR initialized: runtime '" << m_runtime_info.runtime_name
<< "', system '" << m_runtime_info.system_name << "'";
Log(OpenXRLogLevel::Info, message.str());
return true;
}
bool OpenXRRuntime::EnumerateInstanceCapabilities() {
uint32_t extension_count = 0;
if (!Check(xrEnumerateInstanceExtensionProperties(
nullptr, 0, &extension_count, nullptr),
"xrEnumerateInstanceExtensionProperties(count)")) {
return false;
}
std::vector<XrExtensionProperties> extension_properties(
extension_count, XrExtensionProperties{XR_TYPE_EXTENSION_PROPERTIES});
if (extension_count != 0 &&
!Check(xrEnumerateInstanceExtensionProperties(
nullptr, extension_count, &extension_count,
extension_properties.data()),
"xrEnumerateInstanceExtensionProperties")) {
return false;
}
m_available_extensions.clear();
m_available_extensions.reserve(extension_count);
for (const XrExtensionProperties& extension : extension_properties) {
m_available_extensions.emplace_back(extension.extensionName);
}
uint32_t layer_count = 0;
if (!Check(xrEnumerateApiLayerProperties(0, &layer_count, nullptr),
"xrEnumerateApiLayerProperties(count)")) {
return false;
}
std::vector<XrApiLayerProperties> layer_properties(
layer_count, XrApiLayerProperties{XR_TYPE_API_LAYER_PROPERTIES});
if (layer_count != 0 &&
!Check(xrEnumerateApiLayerProperties(
layer_count, &layer_count, layer_properties.data()),
"xrEnumerateApiLayerProperties")) {
return false;
}
m_available_api_layers.clear();
m_available_api_layers.reserve(layer_count);
for (const XrApiLayerProperties& layer : layer_properties) {
m_available_api_layers.emplace_back(layer.layerName);
}
return true;
}
bool OpenXRRuntime::CreateInstance() {
m_enabled_extensions.clear();
for (const std::string& extension : m_config.required_extensions) {
if (!Contains(m_available_extensions, extension)) {
return Fail(XR_ERROR_EXTENSION_NOT_PRESENT, "xrCreateInstance",
"required extension is unavailable: " + extension);
}
if (!Contains(m_enabled_extensions, extension)) {
m_enabled_extensions.push_back(extension);
}
}
for (const std::string& extension : m_config.optional_extensions) {
if (Contains(m_available_extensions, extension) &&
!Contains(m_enabled_extensions, extension)) {
m_enabled_extensions.push_back(extension);
}
}
m_enabled_api_layers.clear();
for (const std::string& layer : m_config.required_api_layers) {
if (!Contains(m_available_api_layers, layer)) {
return Fail(XR_ERROR_API_LAYER_NOT_PRESENT, "xrCreateInstance",
"required API layer is unavailable: " + layer);
}
if (!Contains(m_enabled_api_layers, layer)) {
m_enabled_api_layers.push_back(layer);
}
}
for (const std::string& layer : m_config.optional_api_layers) {
if (Contains(m_available_api_layers, layer) &&
!Contains(m_enabled_api_layers, layer)) {
m_enabled_api_layers.push_back(layer);
}
}
std::vector<const char*> extension_names;
extension_names.reserve(m_enabled_extensions.size());
for (const std::string& extension : m_enabled_extensions) {
extension_names.push_back(extension.c_str());
}
std::vector<const char*> layer_names;
layer_names.reserve(m_enabled_api_layers.size());
for (const std::string& layer : m_enabled_api_layers) {
layer_names.push_back(layer.c_str());
}
XrInstanceCreateInfo create_info{XR_TYPE_INSTANCE_CREATE_INFO};
CopyOpenXRName(create_info.applicationInfo.applicationName,
XR_MAX_APPLICATION_NAME_SIZE, m_config.application_name);
create_info.applicationInfo.applicationVersion = m_config.application_version;
CopyOpenXRName(create_info.applicationInfo.engineName,
XR_MAX_ENGINE_NAME_SIZE, m_config.engine_name);
create_info.applicationInfo.engineVersion = m_config.engine_version;
create_info.applicationInfo.apiVersion = m_config.api_version;
create_info.enabledExtensionCount =
static_cast<uint32_t>(extension_names.size());
create_info.enabledExtensionNames = extension_names.data();
create_info.enabledApiLayerCount = static_cast<uint32_t>(layer_names.size());
create_info.enabledApiLayerNames = layer_names.data();
if (!Check(xrCreateInstance(&create_info, &m_instance), "xrCreateInstance")) {
return false;
}
XrInstanceProperties instance_properties{XR_TYPE_INSTANCE_PROPERTIES};
if (!Check(xrGetInstanceProperties(m_instance, &instance_properties),
"xrGetInstanceProperties")) {
return false;
}
m_runtime_info.runtime_name = instance_properties.runtimeName;
m_runtime_info.runtime_version = instance_properties.runtimeVersion;
return true;
}
bool OpenXRRuntime::InitializeSystem() {
XrSystemGetInfo get_info{XR_TYPE_SYSTEM_GET_INFO};
get_info.formFactor = m_config.form_factor;
if (!Check(xrGetSystem(m_instance, &get_info, &m_system_id), "xrGetSystem")) {
return false;
}
XrSystemProperties properties{XR_TYPE_SYSTEM_PROPERTIES};
if (!Check(xrGetSystemProperties(m_instance, m_system_id, &properties),
"xrGetSystemProperties")) {
return false;
}
m_runtime_info.system_name = properties.systemName;
m_runtime_info.vendor_id = properties.vendorId;
m_runtime_info.max_layer_count = properties.graphicsProperties.maxLayerCount;
m_runtime_info.supports_orientation_tracking =
properties.trackingProperties.orientationTracking == XR_TRUE;
m_runtime_info.supports_position_tracking =
properties.trackingProperties.positionTracking == XR_TRUE;
return true;
}
bool OpenXRRuntime::EnumerateViewConfiguration() {
uint32_t view_count = 0;
if (!Check(xrEnumerateViewConfigurationViews(
m_instance, m_system_id, m_config.view_configuration,
0, &view_count, nullptr),
"xrEnumerateViewConfigurationViews(count)")) {
return false;
}
if (view_count != kOpenXREyeCount) {
std::ostringstream detail;
detail << "PRIMARY_STEREO must expose exactly " << kOpenXREyeCount
<< " views, runtime returned " << view_count;
return Fail(XR_ERROR_VIEW_CONFIGURATION_TYPE_UNSUPPORTED,
"xrEnumerateViewConfigurationViews", detail.str());
}
std::array<XrViewConfigurationView, kOpenXREyeCount> properties{};
for (XrViewConfigurationView& property : properties) {
property.type = XR_TYPE_VIEW_CONFIGURATION_VIEW;
}
if (!Check(xrEnumerateViewConfigurationViews(
m_instance, m_system_id, m_config.view_configuration,
view_count, &view_count, properties.data()),
"xrEnumerateViewConfigurationViews")) {
return false;
}
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
OpenXRViewConfiguration& destination = m_view_configuration[eye];
destination.properties = properties[eye];
destination.render_width = ScaledDimension(
properties[eye].recommendedImageRectWidth,
properties[eye].maxImageRectWidth, m_config.resolution_scale);
destination.render_height = ScaledDimension(
properties[eye].recommendedImageRectHeight,
properties[eye].maxImageRectHeight, m_config.resolution_scale);
}
return true;
}
bool OpenXRRuntime::SelectEnvironmentBlendMode() {
uint32_t blend_mode_count = 0;
if (!Check(xrEnumerateEnvironmentBlendModes(
m_instance, m_system_id, m_config.view_configuration,
0, &blend_mode_count, nullptr),
"xrEnumerateEnvironmentBlendModes(count)")) {
return false;
}
if (blend_mode_count == 0) {
return Fail(XR_ERROR_ENVIRONMENT_BLEND_MODE_UNSUPPORTED,
"xrEnumerateEnvironmentBlendModes",
"runtime returned no environment blend modes");
}
m_supported_blend_modes.resize(blend_mode_count);
if (!Check(xrEnumerateEnvironmentBlendModes(
m_instance, m_system_id, m_config.view_configuration,
blend_mode_count, &blend_mode_count,
m_supported_blend_modes.data()),
"xrEnumerateEnvironmentBlendModes")) {
return false;
}
m_supported_blend_modes.resize(blend_mode_count);
if (Contains(m_supported_blend_modes, m_config.preferred_blend_mode)) {
m_blend_mode = m_config.preferred_blend_mode;
return true;
}
if (Contains(m_supported_blend_modes, XR_ENVIRONMENT_BLEND_MODE_OPAQUE)) {
m_blend_mode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
} else {
m_blend_mode = m_supported_blend_modes.front();
}
Log(OpenXRLogLevel::Warning,
"preferred environment blend mode is unavailable; using runtime fallback");
return true;
}
bool OpenXRRuntime::CreateSession(const void* graphics_binding) {
ClearError();
if (!IsInitialized()) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "CreateSession",
"Initialize must succeed first");
}
if (HasSession()) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "CreateSession",
"a session already exists");
}
if (graphics_binding == nullptr) {
return Fail(XR_ERROR_GRAPHICS_DEVICE_INVALID, "CreateSession",
"graphics binding must not be null");
}
XrSessionCreateInfo create_info{XR_TYPE_SESSION_CREATE_INFO};
create_info.next = graphics_binding;
create_info.systemId = m_system_id;
if (!Check(xrCreateSession(m_instance, &create_info, &m_session),
"xrCreateSession")) {
m_session = XR_NULL_HANDLE;
return false;
}
if (!CreateReferenceSpaces() || !EnumerateSwapchainFormats()) {
DestroyReferenceSpaces();
xrDestroySession(m_session);
m_session = XR_NULL_HANDLE;
ResetSessionState();
return false;
}
m_session_state = XR_SESSION_STATE_UNKNOWN;
m_exit_requested = false;
m_instance_loss_pending = false;
Log(OpenXRLogLevel::Info,
"OpenXR session created; waiting for the runtime READY event");
return true;
}
bool OpenXRRuntime::GetInstanceProcAddress(
const char* name, PFN_xrVoidFunction* function) {
ClearError();
if (!IsInitialized()) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "xrGetInstanceProcAddr",
"OpenXR is not initialized");
}
if (name == nullptr || name[0] == '\0' || function == nullptr) {
return Fail(XR_ERROR_VALIDATION_FAILURE, "xrGetInstanceProcAddr",
"function name and output pointer must be valid");
}
*function = nullptr;
return Check(xrGetInstanceProcAddr(m_instance, name, function),
"xrGetInstanceProcAddr");
}
bool OpenXRRuntime::CreateReferenceSpaces() {
uint32_t space_count = 0;
if (!Check(xrEnumerateReferenceSpaces(m_session, 0, &space_count, nullptr),
"xrEnumerateReferenceSpaces(count)")) {
return false;
}
m_supported_reference_spaces.resize(space_count);
if (space_count != 0 &&
!Check(xrEnumerateReferenceSpaces(
m_session, space_count, &space_count,
m_supported_reference_spaces.data()),
"xrEnumerateReferenceSpaces")) {
return false;
}
m_supported_reference_spaces.resize(space_count);
XrReferenceSpaceCreateInfo view_info{XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
view_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW;
view_info.poseInReferenceSpace = IdentityPose();
if (!Check(xrCreateReferenceSpace(m_session, &view_info, &m_view_space),
"xrCreateReferenceSpace(VIEW)")) {
return false;
}
m_app_space_type = m_config.reference_space;
if (!Contains(m_supported_reference_spaces, m_app_space_type)) {
if (Contains(m_supported_reference_spaces, XR_REFERENCE_SPACE_TYPE_LOCAL)) {
m_app_space_type = XR_REFERENCE_SPACE_TYPE_LOCAL;
Log(OpenXRLogLevel::Warning,
"requested reference space is unavailable; using LOCAL");
} else if (Contains(m_supported_reference_spaces,
XR_REFERENCE_SPACE_TYPE_STAGE)) {
m_app_space_type = XR_REFERENCE_SPACE_TYPE_STAGE;
Log(OpenXRLogLevel::Warning,
"requested reference space is unavailable; using STAGE");
} else {
return Fail(XR_ERROR_REFERENCE_SPACE_UNSUPPORTED,
"xrCreateReferenceSpace",
"runtime exposes neither requested, LOCAL, nor STAGE space");
}
}
XrReferenceSpaceCreateInfo app_info{XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
app_info.referenceSpaceType = m_app_space_type;
app_info.poseInReferenceSpace = IdentityPose();
return Check(xrCreateReferenceSpace(m_session, &app_info, &m_app_space),
"xrCreateReferenceSpace(application)");
}
bool OpenXRRuntime::EnumerateSwapchainFormats() {
uint32_t format_count = 0;
if (!Check(xrEnumerateSwapchainFormats(
m_session, 0, &format_count, nullptr),
"xrEnumerateSwapchainFormats(count)")) {
return false;
}
if (format_count == 0) {
return Fail(XR_ERROR_SWAPCHAIN_FORMAT_UNSUPPORTED,
"xrEnumerateSwapchainFormats",
"runtime returned no swapchain formats");
}
m_swapchain_formats.resize(format_count);
if (!Check(xrEnumerateSwapchainFormats(
m_session, format_count, &format_count,
m_swapchain_formats.data()),
"xrEnumerateSwapchainFormats")) {
return false;
}
m_swapchain_formats.resize(format_count);
return true;
}
OpenXREventStatus OpenXRRuntime::PollEvents() {
if (!IsInitialized()) {
Fail(XR_ERROR_CALL_ORDER_INVALID, "PollEvents",
"OpenXR is not initialized");
return OpenXREventStatus::Error;
}
for (;;) {
XrEventDataBuffer event{XR_TYPE_EVENT_DATA_BUFFER};
const XrResult result = xrPollEvent(m_instance, &event);
if (result == XR_EVENT_UNAVAILABLE) {
return ShouldExit() ? OpenXREventStatus::ExitRequested
: OpenXREventStatus::Continue;
}
if (XR_FAILED(result)) {
Check(result, "xrPollEvent");
if (result == XR_ERROR_INSTANCE_LOST) {
m_instance_loss_pending = true;
}
return OpenXREventStatus::Error;
}
switch (event.type) {
case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: {
const auto& state_event =
*reinterpret_cast<const XrEventDataSessionStateChanged*>(&event);
if (state_event.session == m_session &&
!HandleSessionStateChanged(state_event)) {
return OpenXREventStatus::Error;
}
break;
}
case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING:
m_instance_loss_pending = true;
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;
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;
}
break;
}
case XR_TYPE_EVENT_DATA_EVENTS_LOST: {
const auto& lost_event =
*reinterpret_cast<const XrEventDataEventsLost*>(&event);
std::ostringstream message;
message << "OpenXR runtime lost " << lost_event.lostEventCount
<< " event(s)";
Log(OpenXRLogLevel::Warning, message.str());
break;
}
default:
break;
}
}
}
bool OpenXRRuntime::HandleSessionStateChanged(
const XrEventDataSessionStateChanged& event) {
m_session_state = event.state;
std::ostringstream message;
message << "OpenXR session state -> " << SessionStateName(event.state);
Log(OpenXRLogLevel::Info, message.str());
switch (event.state) {
case XR_SESSION_STATE_READY: {
if (m_shutting_down_session || m_session_running) {
return true;
}
XrSessionBeginInfo begin_info{XR_TYPE_SESSION_BEGIN_INFO};
begin_info.primaryViewConfigurationType = m_config.view_configuration;
if (!Check(xrBeginSession(m_session, &begin_info), "xrBeginSession")) {
return false;
}
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;
}
return true;
case XR_SESSION_STATE_EXITING:
m_exit_requested = true;
m_session_running = false;
return true;
case XR_SESSION_STATE_LOSS_PENDING:
m_instance_loss_pending = true;
m_session_running = false;
return true;
default:
return true;
}
}
bool OpenXRRuntime::RequestExitSession() {
ClearError();
if (!HasSession() || !m_session_running) {
return true;
}
return Check(xrRequestExitSession(m_session), "xrRequestExitSession");
}
OpenXRFrameStatus OpenXRRuntime::WaitFrame(OpenXRFrame& frame) {
ClearError();
if (ShouldExit()) {
return OpenXRFrameStatus::ExitRequested;
}
if (!HasSession() || !m_session_running) {
return OpenXRFrameStatus::SessionNotRunning;
}
if (m_frame_phase != FramePhase::Idle) {
Fail(XR_ERROR_CALL_ORDER_INVALID, "xrWaitFrame",
"the previous frame has not been ended");
return OpenXRFrameStatus::Error;
}
XrFrameWaitInfo wait_info{XR_TYPE_FRAME_WAIT_INFO};
XrFrameState state{XR_TYPE_FRAME_STATE};
if (!Check(xrWaitFrame(m_session, &wait_info, &state), "xrWaitFrame")) {
return OpenXRFrameStatus::Error;
}
frame = {};
frame.serial = m_next_frame_serial++;
frame.predicted_display_time = state.predictedDisplayTime;
frame.predicted_display_period = state.predictedDisplayPeriod;
frame.should_render = state.shouldRender == XR_TRUE;
m_active_frame_serial = frame.serial;
m_active_frame_display_time = frame.predicted_display_time;
m_frame_phase = FramePhase::Waited;
return OpenXRFrameStatus::Ready;
}
bool OpenXRRuntime::BeginFrame(const OpenXRFrame& frame) {
ClearError();
if (!IsFrameTokenCurrent(frame, FramePhase::Waited)) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "xrBeginFrame",
"frame token is stale or xrWaitFrame was not called");
}
XrFrameBeginInfo begin_info{XR_TYPE_FRAME_BEGIN_INFO};
const XrResult result = xrBeginFrame(m_session, &begin_info);
if (XR_FAILED(result)) {
m_frame_phase = FramePhase::Idle;
m_active_frame_serial = 0;
m_active_frame_display_time = 0;
return Check(result, "xrBeginFrame");
}
m_frame_phase = FramePhase::Begun;
return true;
}
bool OpenXRRuntime::LocateViews(OpenXRFrame& frame) {
ClearError();
if (!IsFrameTokenCurrent(frame, FramePhase::Begun)) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "xrLocateViews",
"frame token is stale or xrBeginFrame was not called");
}
frame.views_valid = false;
frame.view_state_flags = 0;
if (!frame.should_render) {
return true;
}
for (XrView& view : frame.views) {
view = {XR_TYPE_VIEW};
}
XrViewLocateInfo locate_info{XR_TYPE_VIEW_LOCATE_INFO};
locate_info.viewConfigurationType = m_config.view_configuration;
locate_info.displayTime = frame.predicted_display_time;
locate_info.space = m_app_space;
XrViewState view_state{XR_TYPE_VIEW_STATE};
uint32_t view_count = 0;
if (!Check(xrLocateViews(m_session, &locate_info, &view_state,
kOpenXREyeCount, &view_count, frame.views.data()),
"xrLocateViews")) {
return false;
}
if (view_count != kOpenXREyeCount) {
return Fail(XR_ERROR_RUNTIME_FAILURE, "xrLocateViews",
"runtime returned an unexpected stereo view count");
}
frame.view_state_flags = view_state.viewStateFlags;
frame.views_valid =
(view_state.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) != 0;
return true;
}
bool OpenXRRuntime::EndFrame(
const OpenXRFrame& frame,
const XrCompositionLayerBaseHeader* const* layers,
uint32_t layer_count) {
ClearError();
if (!IsFrameTokenCurrent(frame, FramePhase::Begun)) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "xrEndFrame",
"frame token is stale or xrBeginFrame was not called");
}
if (layer_count != 0 && layers == nullptr) {
return Fail(XR_ERROR_VALIDATION_FAILURE, "xrEndFrame",
"non-zero layer_count requires a layer array");
}
// The runtime explicitly requested no application rendering. Ending with an
// empty layer list preserves the frame protocol without presenting stale work.
if (!frame.should_render) {
layers = nullptr;
layer_count = 0;
}
XrFrameEndInfo end_info{XR_TYPE_FRAME_END_INFO};
end_info.displayTime = frame.predicted_display_time;
end_info.environmentBlendMode = m_blend_mode;
end_info.layerCount = layer_count;
end_info.layers = layers;
const XrResult result = xrEndFrame(m_session, &end_info);
m_frame_phase = FramePhase::Idle;
m_active_frame_serial = 0;
m_active_frame_display_time = 0;
return Check(result, "xrEndFrame");
}
bool OpenXRRuntime::EndFrame(
const OpenXRFrame& frame,
const std::vector<const XrCompositionLayerBaseHeader*>& layers) {
return EndFrame(frame, layers.data(), static_cast<uint32_t>(layers.size()));
}
bool OpenXRRuntime::EndFrameWithoutLayers(const OpenXRFrame& frame) {
return EndFrame(frame, nullptr, 0);
}
bool OpenXRRuntime::ResetAppSpace(const XrPosef& pose_in_reference_space) {
ClearError();
if (!HasSession()) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "ResetAppSpace",
"no OpenXR session exists");
}
if (m_frame_phase != FramePhase::Idle) {
return Fail(XR_ERROR_CALL_ORDER_INVALID, "ResetAppSpace",
"reference space cannot change during a frame");
}
XrReferenceSpaceCreateInfo create_info{XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
create_info.referenceSpaceType = m_app_space_type;
create_info.poseInReferenceSpace = pose_in_reference_space;
XrSpace replacement = XR_NULL_HANDLE;
if (!Check(xrCreateReferenceSpace(m_session, &create_info, &replacement),
"xrCreateReferenceSpace(recenter)")) {
return false;
}
if (m_app_space != XR_NULL_HANDLE) {
xrDestroySpace(m_app_space);
}
m_app_space = replacement;
return true;
}
void OpenXRRuntime::DestroySession() {
if (!HasSession()) {
ResetSessionState();
return;
}
if (m_frame_phase == FramePhase::Begun) {
Log(OpenXRLogLevel::Warning,
"ending an active OpenXR frame without layers during teardown");
XrFrameEndInfo end_info{XR_TYPE_FRAME_END_INFO};
end_info.displayTime = m_active_frame_display_time;
end_info.environmentBlendMode = m_blend_mode;
const XrResult end_result = xrEndFrame(m_session, &end_info);
if (XR_FAILED(end_result)) {
Log(OpenXRLogLevel::Warning,
"xrEndFrame failed during session teardown");
}
m_frame_phase = FramePhase::Idle;
m_active_frame_serial = 0;
m_active_frame_display_time = 0;
}
m_shutting_down_session = true;
if (m_session_running) {
const XrResult request_result = xrRequestExitSession(m_session);
if (XR_FAILED(request_result)) {
Log(OpenXRLogLevel::Warning,
"xrRequestExitSession failed during bounded teardown");
} else {
const auto timeout =
std::chrono::milliseconds(m_config.shutdown_timeout_ms);
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (m_session_running &&
std::chrono::steady_clock::now() < deadline) {
if (PollEvents() == OpenXREventStatus::Error) {
break;
}
if (m_session_running) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
if (m_session_running) {
Log(OpenXRLogLevel::Warning,
"OpenXR runtime did not finish session exit before timeout");
}
}
}
DestroyReferenceSpaces();
const XrResult result = xrDestroySession(m_session);
if (XR_FAILED(result)) {
Log(OpenXRLogLevel::Warning, "xrDestroySession failed");
}
m_session = XR_NULL_HANDLE;
ResetSessionState();
}
void OpenXRRuntime::Shutdown() {
DestroySession();
if (m_instance != XR_NULL_HANDLE) {
const XrResult result = xrDestroyInstance(m_instance);
if (XR_FAILED(result)) {
Log(OpenXRLogLevel::Warning, "xrDestroyInstance failed");
}
}
ResetInstanceState();
}
void OpenXRRuntime::DestroyReferenceSpaces() {
if (m_view_space != XR_NULL_HANDLE) {
xrDestroySpace(m_view_space);
m_view_space = XR_NULL_HANDLE;
}
if (m_app_space != XR_NULL_HANDLE) {
xrDestroySpace(m_app_space);
m_app_space = XR_NULL_HANDLE;
}
}
void OpenXRRuntime::ResetSessionState() {
m_session_state = XR_SESSION_STATE_UNKNOWN;
m_app_space_type = m_config.reference_space;
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;
m_supported_reference_spaces.clear();
m_swapchain_formats.clear();
m_reference_space_change = {};
}
void OpenXRRuntime::ResetInstanceState() {
m_instance = XR_NULL_HANDLE;
m_system_id = XR_NULL_SYSTEM_ID;
m_instance_loss_pending = false;
m_runtime_info = {};
m_view_configuration = {};
m_available_extensions.clear();
m_available_api_layers.clear();
m_enabled_extensions.clear();
m_enabled_api_layers.clear();
m_supported_blend_modes.clear();
ResetSessionState();
}
bool OpenXRRuntime::IsFrameTokenCurrent(
const OpenXRFrame& frame, FramePhase expected) const {
return m_frame_phase == expected && frame.serial != 0 &&
frame.serial == m_active_frame_serial;
}
bool OpenXRRuntime::Check(XrResult result, std::string_view operation) {
if (XR_SUCCEEDED(result)) {
return true;
}
return Fail(result, operation, {});
}
bool OpenXRRuntime::Fail(
XrResult result, std::string_view operation, std::string_view detail) {
m_last_error.result = result;
m_last_error.operation.assign(operation);
std::ostringstream message;
message << operation << " failed: " << ResultString(result);
if (!detail.empty()) {
message << " (" << detail << ')';
}
m_last_error.message = message.str();
Log(OpenXRLogLevel::Error, m_last_error.message);
return false;
}
void OpenXRRuntime::ClearError() {
m_last_error = {};
}
void OpenXRRuntime::Log(
OpenXRLogLevel level, std::string_view message) const noexcept {
if (!m_logger) {
return;
}
try {
m_logger(level, message);
} catch (...) {
// Diagnostic callbacks must never make XR teardown throw.
}
}
std::string OpenXRRuntime::ResultString(XrResult result) const {
if (m_instance != XR_NULL_HANDLE) {
char text[XR_MAX_RESULT_STRING_SIZE]{};
if (XR_SUCCEEDED(xrResultToString(m_instance, result, text))) {
return text;
}
}
return std::to_string(static_cast<int32_t>(result));
}
} // namespace mkw::vr