mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 06:00:25 +02:00
Initial OpenXR configuration
This commit is contained in:
1 parent
9a22de1079
commit
0beb7d880c
18 files changed
+1904
-3
No files matched your search
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user