mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 01:00:14 +02:00
Fix : XR thread resubmits the last valid frame during stall + Eager Frame Heartbeat option
This commit is contained in:
1 parent
4cf9f60ee7
commit
f525b9dd75
14 files changed
+667
-89
No files matched your search
@@ -23,6 +23,7 @@ configuration and is created with the following defaults:
|
||||
enabled = false
|
||||
required = false
|
||||
mirror_view = "normal"
|
||||
eager_frame_heartbeat = false
|
||||
render_scale = 1.0
|
||||
world_units_per_meter = 500.0
|
||||
hud_distance_meters = 2.0
|
||||
@@ -55,6 +56,15 @@ sits directly under the enable switch as *Desktop view*. Menus reach the headset
|
||||
screen carrying the desktop image itself, so there is no separate eye view to mirror there and the
|
||||
three eye choices show that same image; only `"none"` differs. The F10 bar is drawn over whichever
|
||||
image is chosen, so the setting can always be changed back.
|
||||
Eye mirror modes retain the last eye image when a desktop frame has no new XR packet, so they
|
||||
do not alternate with the normal camera. `"none"` also stays black between XR packets.
|
||||
|
||||
`eager_frame_heartbeat` is live in **F10 > VR > Eager Frame Heartbeat** and defaults to `false`.
|
||||
With it off, the XR thread waits for new game frames and repeats the last valid image during
|
||||
stalls, with a 50 ms keep-alive interval. With it on, the thread repeats at headset display
|
||||
deadlines while waiting for new rendering, matching the eager behavior. Compare both settings
|
||||
during the same race to check smoothness with your OpenXR runtime. Both retain the protection
|
||||
against black frames during pauses and window dragging.
|
||||
|
||||
`render_scale` scales the per-eye size recommended by the OpenXR runtime.
|
||||
`world_units_per_meter` controls the scale of headset translation in the game world.
|
||||
@@ -135,10 +145,23 @@ receive asymmetric headset projections, while the game's 2D layer goes on a fixe
|
||||
Head pose is sampled by the OpenXR pacing thread, while Aurora's frame worker consumes a
|
||||
short-lived immutable stereo packet. Each sealed GX frame and immersive packet carry the same
|
||||
policy-generation tag; a mismatch is rendered in mono and the acquired XR frame is canceled, so an
|
||||
asynchronous menu/race transition cannot replay race transforms over unsafe content. A pause or
|
||||
minimized window can withdraw an unencoded packet and end that compositor frame without layers; an
|
||||
encoded-work stall requests teardown at Aurora's next safe producer boundary. All OpenXR session
|
||||
and swapchain calls remain on their owning thread.
|
||||
asynchronous menu/race transition cannot replay race transforms over unsafe content.
|
||||
|
||||
The D3D12 pacing thread retains the last completed projection or virtual-screen layer and
|
||||
resubmits it during stalls (or missed display deadlines with eager heartbeat enabled), including while moving the desktop window,
|
||||
pausing, or minimizing. The scene freezes until rendering resumes; the compositor can still
|
||||
reproject the retained image for head movement. Repeated layers keep their original render poses
|
||||
and field of view, paired with the new compositor display time. Two pairs of eye swapchains keep
|
||||
the retained image separate from pending or canceled rendering (at the cost of additional GPU
|
||||
memory). Unencoded packets can be withdrawn after 50 ms; encoded work retains its images while
|
||||
the pacing thread continues submitting the last completed layer. A stall alone no longer requests
|
||||
desktop fallback after 250 ms.
|
||||
|
||||
Before the first valid image, when OpenXR requests no rendering, or after a session/reference-space
|
||||
change invalidates the retained content, frames can still have no layers. Actual runtime or GPU
|
||||
submission failures retain the safe teardown path. This does not detect black images rendered by
|
||||
the game itself, and cannot keep submitting if the entire process or XR runtime is suspended.
|
||||
All OpenXR session and swapchain calls remain on their owning thread.
|
||||
|
||||
## The race's 2D layer
|
||||
|
||||
|
||||
@@ -110,6 +110,8 @@ bool aurora_get_stereo_hud_screen_enabled();
|
||||
// ordinary mono presentation untouched, the eye views mirror what the headset is
|
||||
// actually displaying, and NONE presents a black window. Live, and only
|
||||
// consulted while a stereo frame provider is supplying frames.
|
||||
// When a frame has no new XR packet, eye views retain the previous eye image
|
||||
// instead of falling back to the ordinary desktop view.
|
||||
//
|
||||
// A menu frame reaches the headset as a virtual screen carrying the very mono
|
||||
// image the desktop already shows, so there is no distinct eye view to mirror:
|
||||
|
||||
+11
-30
@@ -8,6 +8,7 @@
|
||||
#include "gx/shader_info.hpp"
|
||||
#include "imgui.hpp"
|
||||
#include "stereo.hpp"
|
||||
#include "stereo_mirror.hpp"
|
||||
#include "webgpu/gpu.hpp"
|
||||
#include <webgpu/webgpu_cpp.h>
|
||||
#endif
|
||||
@@ -544,6 +545,7 @@ struct StereoEyeTarget {
|
||||
const webgpu::TextureWithSampler& output() const noexcept { return resolvedColor.texture ? resolvedColor : color; }
|
||||
};
|
||||
std::array<StereoEyeTarget, AURORA_STEREO_EYE_COUNT> g_stereoEyeTargets;
|
||||
stereo::MirrorState g_stereoMirrorState;
|
||||
|
||||
// The eye targets outlive a frame, so the mirror samples them through a bind
|
||||
// group cached beside them rather than one built per presentation slot.
|
||||
@@ -1422,35 +1424,7 @@ void stop_presenter() noexcept {
|
||||
// view; the rest mirror the headset and are only ever chosen while a stereo
|
||||
// provider is feeding one. ImGui is drawn over all of them alike, so the
|
||||
// settings menu stays reachable even under Black.
|
||||
enum class MirrorPlan {
|
||||
Mono,
|
||||
LeftEye,
|
||||
RightEye,
|
||||
BothEyes,
|
||||
Black,
|
||||
};
|
||||
|
||||
// A virtual-screen (menu) frame puts the desktop's own mono image on both eyes,
|
||||
// so there is no separate eye view to mirror and the eye choices collapse onto
|
||||
// Mono. Only Black still has something distinct to do there.
|
||||
MirrorPlan resolve_mirror_plan(AuroraStereoMirrorView view, bool stereoOutput, bool immersiveReplay) noexcept {
|
||||
if (!stereoOutput) {
|
||||
return MirrorPlan::Mono;
|
||||
}
|
||||
switch (view) {
|
||||
case AURORA_STEREO_MIRROR_NONE:
|
||||
return MirrorPlan::Black;
|
||||
case AURORA_STEREO_MIRROR_BOTH_EYES:
|
||||
return immersiveReplay ? MirrorPlan::BothEyes : MirrorPlan::Mono;
|
||||
case AURORA_STEREO_MIRROR_LEFT_EYE:
|
||||
return immersiveReplay ? MirrorPlan::LeftEye : MirrorPlan::Mono;
|
||||
case AURORA_STEREO_MIRROR_RIGHT_EYE:
|
||||
return immersiveReplay ? MirrorPlan::RightEye : MirrorPlan::Mono;
|
||||
case AURORA_STEREO_MIRROR_NORMAL:
|
||||
break;
|
||||
}
|
||||
return MirrorPlan::Mono;
|
||||
}
|
||||
using stereo::MirrorPlan;
|
||||
|
||||
// Places one eye inside `bounds`, keeping the eye's own aspect ratio rather
|
||||
// than the game's presented one: an eye is already the shape the headset asked
|
||||
@@ -1567,6 +1541,7 @@ void shutdown() noexcept {
|
||||
#ifdef AURORA_ENABLE_GX
|
||||
stop_presenter();
|
||||
g_stereoEyeTargets = {};
|
||||
g_stereoMirrorState.Reset();
|
||||
g_presentationImagePools = {};
|
||||
imgui::shutdown();
|
||||
gfx::shutdown();
|
||||
@@ -1785,7 +1760,8 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
|
||||
const bool immersiveReplay = stereoOutput && ctx.immersiveStereoPrepared;
|
||||
// One choice for the whole group: a slot showing the mono view next to slots
|
||||
// mirroring an eye would strobe between two different images.
|
||||
const MirrorPlan mirrorPlan = resolve_mirror_plan(gfx::get_stereo_mirror_view(), stereoOutput, immersiveReplay);
|
||||
const MirrorPlan mirrorPlan = g_stereoMirrorState.Resolve(
|
||||
gfx::get_stereo_mirror_view(), stereo_frame_provider_active(), stereoOutput, immersiveReplay);
|
||||
|
||||
// Each slot is submitted as soon as it is encoded, so the GPU starts slot 0 while slot 1 is still
|
||||
// recording. Queue order preserves the ordering the single batched buffer gave.
|
||||
@@ -2238,6 +2214,11 @@ bool stereo_frame_provider_active() noexcept { return g_stereoProviderActive.loa
|
||||
|
||||
void set_stereo_frame_provider(AuroraStereoFrameProvider provider, void* userdata) noexcept {
|
||||
std::lock_guard lock(g_stereoRegistrationMutex);
|
||||
#ifdef AURORA_ENABLE_GX
|
||||
// Registration changes require an idle frame worker, so its cached mirror
|
||||
// state can be reset here without racing texture use.
|
||||
g_stereoMirrorState.Reset();
|
||||
#endif
|
||||
g_stereoProvider = {
|
||||
.callback = provider,
|
||||
.userdata = userdata,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <aurora/gfx.h>
|
||||
|
||||
namespace aurora::stereo {
|
||||
|
||||
enum class MirrorPlan { Mono, LeftEye, RightEye, BothEyes, Black };
|
||||
|
||||
// Owned by the frame worker, alongside the persistent eye textures. An absent
|
||||
// XR packet means the headset is retaining its image, not switching to mono.
|
||||
class MirrorState {
|
||||
public:
|
||||
MirrorPlan Resolve(AuroraStereoMirrorView view, bool providerActive,
|
||||
bool freshOutput, bool immersiveReplay) noexcept {
|
||||
if (!providerActive) {
|
||||
Reset();
|
||||
return MirrorPlan::Mono;
|
||||
}
|
||||
if (freshOutput) {
|
||||
m_immersive = immersiveReplay;
|
||||
}
|
||||
switch (view) {
|
||||
case AURORA_STEREO_MIRROR_NONE:
|
||||
return MirrorPlan::Black;
|
||||
case AURORA_STEREO_MIRROR_BOTH_EYES:
|
||||
return m_immersive ? MirrorPlan::BothEyes : MirrorPlan::Mono;
|
||||
case AURORA_STEREO_MIRROR_LEFT_EYE:
|
||||
return m_immersive ? MirrorPlan::LeftEye : MirrorPlan::Mono;
|
||||
case AURORA_STEREO_MIRROR_RIGHT_EYE:
|
||||
return m_immersive ? MirrorPlan::RightEye : MirrorPlan::Mono;
|
||||
default:
|
||||
return MirrorPlan::Mono;
|
||||
}
|
||||
}
|
||||
|
||||
void Reset() noexcept { m_immersive = false; }
|
||||
|
||||
private:
|
||||
bool m_immersive = false;
|
||||
};
|
||||
|
||||
} // namespace aurora::stereo
|
||||
@@ -18,6 +18,7 @@ if (AURORA_ENABLE_GX)
|
||||
gx_fifo_test.cpp
|
||||
gx_test_stubs.cpp
|
||||
stereo_replay_test.cpp
|
||||
stereo_mirror_test.cpp
|
||||
texture_bind_group_cache_key_test.cpp
|
||||
../lib/gfx/efb_ram_encoder.cpp
|
||||
# GX API implementations (encoders)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "stereo_mirror.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace aurora::stereo {
|
||||
|
||||
TEST(StereoMirror, RetainsEyesAcrossMissingPackets) {
|
||||
MirrorState state;
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_LEFT_EYE, true, false, false), MirrorPlan::Mono);
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_LEFT_EYE, true, true, true), MirrorPlan::LeftEye);
|
||||
for (int frame = 0; frame < 120; ++frame) {
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_LEFT_EYE, true, false, false), MirrorPlan::LeftEye);
|
||||
}
|
||||
// Live selection remains usable even without a new headset frame.
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_RIGHT_EYE, true, false, false), MirrorPlan::RightEye);
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_BOTH_EYES, true, false, false), MirrorPlan::BothEyes);
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_NORMAL, true, false, false), MirrorPlan::Mono);
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_LEFT_EYE, true, false, false), MirrorPlan::LeftEye);
|
||||
}
|
||||
|
||||
TEST(StereoMirror, MenusAndSessionEndReplaceRetainedRaceView) {
|
||||
MirrorState state;
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_BOTH_EYES, true, true, true), MirrorPlan::BothEyes);
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_BOTH_EYES, true, true, false), MirrorPlan::Mono);
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_BOTH_EYES, true, false, false), MirrorPlan::Mono);
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_BOTH_EYES, true, true, true), MirrorPlan::BothEyes);
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_BOTH_EYES, false, false, false), MirrorPlan::Mono);
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_BOTH_EYES, true, false, false), MirrorPlan::Mono);
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_LEFT_EYE, true, true, true), MirrorPlan::LeftEye);
|
||||
state.Reset(); // Provider replacement must not reuse the previous session.
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_LEFT_EYE, true, false, false), MirrorPlan::Mono);
|
||||
}
|
||||
|
||||
TEST(StereoMirror, NoneStaysBlackWithoutFreshFrames) {
|
||||
MirrorState state;
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_NONE, true, true, true), MirrorPlan::Black);
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_NONE, true, false, false), MirrorPlan::Black);
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_NONE, true, true, false), MirrorPlan::Black);
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_NONE, true, false, false), MirrorPlan::Black);
|
||||
EXPECT_EQ(state.Resolve(AURORA_STEREO_MIRROR_NONE, false, false, false), MirrorPlan::Mono);
|
||||
}
|
||||
|
||||
} // namespace aurora::stereo
|
||||
@@ -342,6 +342,19 @@ target_include_directories(mkw_vr_first_person_tests PRIVATE "${CMAKE_CURRENT_LI
|
||||
target_compile_features(mkw_vr_first_person_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_vr_first_person_tests COMMAND mkw_vr_first_person_tests)
|
||||
|
||||
if(MKW_ENABLE_OPENXR AND MKW_PLATFORM_WINDOWS)
|
||||
add_executable(mkw_openxr_replay_tests
|
||||
tests/openxr_d3d12_replay_tests.cpp src/vr/openxr_d3d12.cpp)
|
||||
target_include_directories(mkw_openxr_replay_tests PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../aurora-main/include"
|
||||
"$<TARGET_PROPERTY:${MKW_OPENXR_TARGET},INTERFACE_INCLUDE_DIRECTORIES>")
|
||||
target_compile_definitions(mkw_openxr_replay_tests PRIVATE MKW_ENABLE_OPENXR=1)
|
||||
target_compile_features(mkw_openxr_replay_tests PRIVATE cxx_std_17)
|
||||
set_target_properties(mkw_openxr_replay_tests PROPERTIES UNITY_BUILD OFF)
|
||||
add_test(NAME mkw_openxr_replay_tests COMMAND mkw_openxr_replay_tests)
|
||||
endif()
|
||||
|
||||
add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp")
|
||||
target_include_directories(mkw_nand_save_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
|
||||
|
||||
@@ -57,6 +57,7 @@ struct RuntimeUserConfig {
|
||||
std::optional<bool> vrStopAtDisplayCopy;
|
||||
std::optional<bool> vrSkipCopyClears;
|
||||
std::optional<std::string> vrMirrorView;
|
||||
std::optional<bool> vrEagerFrameHeartbeat;
|
||||
std::optional<bool> vrFirstPerson;
|
||||
std::optional<float> vrFirstPersonUnitsPerMeter;
|
||||
std::optional<float> vrFirstPersonHeadUpMeters;
|
||||
@@ -375,6 +376,8 @@ inline void EnsureConfigFile() {
|
||||
"# \"right\" mirror the headset's eyes, and \"none\" blacks the window\n"
|
||||
"# out. Changeable live from the F10 menu.\n"
|
||||
"mirror_view = \"normal\"\n"
|
||||
"# Repeat at headset cadence (true), or only during stalls (false). Live.\n"
|
||||
"eager_frame_heartbeat = false\n"
|
||||
"render_scale = 1.0\n"
|
||||
"world_units_per_meter = 500.0\n"
|
||||
"hud_distance_meters = 2.0\n"
|
||||
@@ -635,6 +638,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
value && IsSupportedVrMirrorView(*value)) {
|
||||
config.vrMirrorView = *value;
|
||||
}
|
||||
config.vrEagerFrameHeartbeat = FindConfigValue<bool>(document, "vr", "eager_frame_heartbeat");
|
||||
if (auto value = FindConfigInt(document, "vr", "first_person_hidden_model");
|
||||
value && *value >= -1 && *value <= 31) {
|
||||
config.vrFirstPersonHiddenModel = static_cast<int32_t>(*value);
|
||||
@@ -936,6 +940,11 @@ inline bool SetVrMirrorView(std::string value) {
|
||||
return WriteSetting("vr", "mirror_view", FormatString(value));
|
||||
}
|
||||
|
||||
inline bool SetVrEagerFrameHeartbeat(bool value) {
|
||||
Mutable().vrEagerFrameHeartbeat = value;
|
||||
return WriteSetting("vr", "eager_frame_heartbeat", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonRotation(std::string value) {
|
||||
if (!IsSupportedVrFirstPersonRotation(value)) {
|
||||
return false;
|
||||
@@ -1268,6 +1277,10 @@ inline std::string VrMirrorView(std::string fallback = kVrMirrorViewDefault) {
|
||||
return value && IsSupportedVrMirrorView(*value) ? *value : std::move(fallback);
|
||||
}
|
||||
|
||||
inline bool VrEagerFrameHeartbeat() {
|
||||
return Get().vrEagerFrameHeartbeat.value_or(false);
|
||||
}
|
||||
|
||||
inline std::string VrFirstPersonRotation(std::string fallback = kVrFirstPersonRotationDefault) {
|
||||
const auto& value = Get().vrFirstPersonRotation;
|
||||
return value && IsSupportedVrFirstPersonRotation(*value) ? *value : std::move(fallback);
|
||||
|
||||
@@ -92,7 +92,7 @@ public:
|
||||
|
||||
// timeout_ms == UINT32_MAX waits until Aurora publishes this token or
|
||||
// Shutdown() interrupts the wait. A timeout does not release XR images;
|
||||
// the caller must first drain/cancel Aurora, then FinishFrame(false).
|
||||
// the caller may keep pacing with RepeatFrame while Aurora still owns them.
|
||||
OpenXRD3D12SubmissionStatus WaitForSubmission(const OpenXRD3D12Frame& frame,
|
||||
uint32_t timeout_ms = UINT32_MAX);
|
||||
|
||||
@@ -101,11 +101,17 @@ public:
|
||||
// is required to release them and close the compositor frame.
|
||||
bool TryCancelPendingFrame(OpenXRD3D12Frame& frame);
|
||||
|
||||
// Ends the current compositor cycle with the last completed layer and starts
|
||||
// another, without releasing or changing Aurora's pending images/render token.
|
||||
// The original render poses remain attached to the pending and retained images.
|
||||
bool RepeatFrame(const OpenXRD3D12Frame& frame);
|
||||
|
||||
// Releases acquired images and calls xrEndFrame. submit_layer must only be
|
||||
// true after WaitForSubmission returned Success. Immersive frames submit
|
||||
// XrCompositionLayerProjection; virtual-screen frames submit an
|
||||
// XrCompositionLayerQuad using the single mono target, placed as the
|
||||
// presentation's quad_anchored/quad_pose describe.
|
||||
// presentation's quad_anchored/quad_pose describe. If no new usable layer is
|
||||
// available, resubmits the retained layer using the current display time.
|
||||
bool FinishFrame(OpenXRD3D12Frame& frame, bool submit_layer);
|
||||
|
||||
// Call on the XR owner thread after Aurora's worker is idle and before
|
||||
|
||||
@@ -53,4 +53,8 @@ void OpenXRRequestRecenter() noexcept;
|
||||
// once per published frame.
|
||||
void OpenXRSetLeanBackDegrees(float degrees) noexcept;
|
||||
|
||||
// Live pacing choice. Disabled: submit fresh frames promptly and repeat during
|
||||
// stalls. Enabled: repeat at each headset display deadline while waiting.
|
||||
void OpenXRSetEagerFrameHeartbeat(bool enabled) noexcept;
|
||||
|
||||
} // namespace mkw::vr
|
||||
@@ -117,6 +117,7 @@ float g_vrFirstPersonHeadUp = RuntimeConfigFile::VrFirstPersonHeadUpMeters();
|
||||
float g_vrFirstPersonHeadForward = RuntimeConfigFile::VrFirstPersonHeadForwardMeters();
|
||||
float g_vrFirstPersonHeadRight = RuntimeConfigFile::VrFirstPersonHeadRightMeters();
|
||||
bool g_vrFirstPersonHideDriver = RuntimeConfigFile::VrFirstPersonHideDriver();
|
||||
bool g_vrEagerFrameHeartbeat = RuntimeConfigFile::VrEagerFrameHeartbeat();
|
||||
int g_vrFirstPersonHiddenModel = RuntimeConfigFile::VrFirstPersonHiddenModel();
|
||||
// Config spellings and menu labels for the desktop mirror, index-matched to
|
||||
// AuroraStereoMirrorView so the combo selection converts to either directly.
|
||||
@@ -925,6 +926,17 @@ void DrawVrSettings() {
|
||||
"same desktop image, so the eye choices only differ from Normal during a race.");
|
||||
}
|
||||
|
||||
if (ImGui::Checkbox("Eager Frame Heartbeat", &g_vrEagerFrameHeartbeat)) {
|
||||
mkw::vr::OpenXRSetEagerFrameHeartbeat(g_vrEagerFrameHeartbeat);
|
||||
RuntimeConfigFile::SetVrEagerFrameHeartbeat(g_vrEagerFrameHeartbeat);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"On: repeat the last frame at headset refresh rate while waiting for the game. "
|
||||
"Off (default): follow the game's frame rate, with repeats during stalls. "
|
||||
"Compare both during a race to check headset smoothness. Applies immediately.");
|
||||
}
|
||||
|
||||
// Like the mirror above and unlike the enable toggle, these two apply to the
|
||||
// very next frame, so they can be compared from inside a running race.
|
||||
ImGui::Separator();
|
||||
|
||||
+104
-34
@@ -261,6 +261,8 @@ public:
|
||||
frame_active_ = true;
|
||||
active_frame_serial_ = frame.xr_frame.serial;
|
||||
active_frame_ = frame.xr_frame;
|
||||
render_session_serial_ = runtime_->SessionRunSerial();
|
||||
render_space_serial_ = runtime_->LastReferenceSpaceChange().serial;
|
||||
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
frame.render_width[eye] = eye_swapchains_[eye].width;
|
||||
@@ -393,22 +395,83 @@ public:
|
||||
const bool can_submit = submit_layer && release_ok && frame.xr_frame.should_render &&
|
||||
frame.xr_frame.views_valid && frame.expects_gpu_submission &&
|
||||
composition_pose_valid;
|
||||
bool end_ok = false;
|
||||
if (can_submit) {
|
||||
// xrEndFrame references the MOST RECENTLY RELEASED image of a
|
||||
// swapchain, not an explicit image index. Keep the displayed pair
|
||||
// separate from the pair Aurora can write or cancel next.
|
||||
std::swap(eye_swapchains_, retained_swapchains_);
|
||||
retained_frame_ = frame;
|
||||
retained_session_serial_ = render_session_serial_;
|
||||
retained_space_serial_ = render_space_serial_;
|
||||
have_retained_frame_ = true;
|
||||
}
|
||||
const bool end_ok = EndRetainedFrame();
|
||||
|
||||
frame_active_ = false;
|
||||
active_frame_serial_ = 0;
|
||||
active_frame_ = {};
|
||||
frame.expects_gpu_submission = false;
|
||||
{
|
||||
std::lock_guard lock(submission_mutex_);
|
||||
awaiting_token_ = 0;
|
||||
submission_arrived_ = false;
|
||||
submission_success_ = false;
|
||||
submission_unsafe_ = false;
|
||||
}
|
||||
return release_ok && end_ok;
|
||||
}
|
||||
|
||||
bool RepeatFrame(const OpenXRD3D12Frame& frame) {
|
||||
if (!frame_active_ || runtime_ == nullptr ||
|
||||
frame.xr_frame.serial != active_frame_serial_) {
|
||||
return Fail("RepeatFrame received a stale or inactive render token");
|
||||
}
|
||||
const bool end_ok = EndRetainedFrame();
|
||||
// EndFrame consumes the compositor token even when submission fails.
|
||||
// Teardown must not try to end that same token again.
|
||||
frame_active_ = false;
|
||||
if (!end_ok) {
|
||||
return Fail("OpenXR could not resubmit the retained frame");
|
||||
}
|
||||
// active_frame_serial_ continues to identify Aurora's pending render;
|
||||
// active_frame_ identifies the independently advancing compositor cycle.
|
||||
if (runtime_->PollEvents() != OpenXREventStatus::Continue ||
|
||||
!runtime_->IsSessionRunning() || runtime_->ShouldExit()) {
|
||||
return Fail("OpenXR session stopped while waiting for stereo rendering");
|
||||
}
|
||||
if (runtime_->WaitFrame(active_frame_) != OpenXRFrameStatus::Ready ||
|
||||
!runtime_->BeginFrame(active_frame_)) {
|
||||
return Fail("OpenXR could not start a retained-frame compositor cycle");
|
||||
}
|
||||
frame_active_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EndRetainedFrame() {
|
||||
if (!runtime_->IsSessionRunning()) {
|
||||
// A session that is no longer running needs no compositor frame
|
||||
// completion call. Preserve the original backend failure instead
|
||||
// of replacing it with a stale-token error.
|
||||
end_ok = true;
|
||||
} else if (!can_submit) {
|
||||
end_ok = runtime_->EndFrameWithoutLayers(frame.xr_frame);
|
||||
} else if (frame.presentation.mode == OpenXRD3D12FrameMode::VirtualScreen) {
|
||||
return true;
|
||||
}
|
||||
// Old poses cannot be reused after the runtime changes their coordinate
|
||||
// system. Also discard content across session restarts.
|
||||
if (retained_session_serial_ != runtime_->SessionRunSerial() ||
|
||||
retained_space_serial_ != runtime_->LastReferenceSpaceChange().serial) {
|
||||
have_retained_frame_ = false;
|
||||
}
|
||||
if (!have_retained_frame_ || !active_frame_.should_render) {
|
||||
return runtime_->EndFrameWithoutLayers(active_frame_);
|
||||
}
|
||||
const auto& frame = retained_frame_;
|
||||
if (frame.presentation.mode == OpenXRD3D12FrameMode::VirtualScreen) {
|
||||
XrCompositionLayerQuad quad{XR_TYPE_COMPOSITION_LAYER_QUAD};
|
||||
quad.layerFlags = 0;
|
||||
quad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
quad.subImage.swapchain = eye_swapchains_[0].handle;
|
||||
quad.subImage.swapchain = retained_swapchains_[0].handle;
|
||||
quad.subImage.imageRect = {{0, 0},
|
||||
{static_cast<int32_t>(eye_swapchains_[0].width),
|
||||
static_cast<int32_t>(eye_swapchains_[0].height)}};
|
||||
{static_cast<int32_t>(retained_swapchains_[0].width),
|
||||
static_cast<int32_t>(retained_swapchains_[0].height)}};
|
||||
quad.subImage.imageArrayIndex = 0;
|
||||
if (frame.presentation.quad_anchored) {
|
||||
// Placed in the application space, so the screen keeps its place
|
||||
@@ -424,25 +487,23 @@ public:
|
||||
0.0f, 0.0f, -std::max(0.25f, frame.presentation.quad_distance_meters)};
|
||||
}
|
||||
quad.size.width = std::max(0.25f, frame.presentation.quad_width_meters);
|
||||
quad.size.height = quad.size.width * static_cast<float>(eye_swapchains_[0].height) /
|
||||
static_cast<float>(eye_swapchains_[0].width);
|
||||
quad.size.height = quad.size.width * static_cast<float>(retained_swapchains_[0].height) /
|
||||
static_cast<float>(retained_swapchains_[0].width);
|
||||
const XrCompositionLayerBaseHeader* layers[] = {
|
||||
reinterpret_cast<const XrCompositionLayerBaseHeader*>(&quad)};
|
||||
end_ok = runtime_->EndFrame(frame.xr_frame, layers, 1);
|
||||
return runtime_->EndFrame(active_frame_, layers, 1);
|
||||
} else {
|
||||
std::array<XrCompositionLayerProjectionView, kOpenXREyeCount> views{};
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
views[eye] = {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW};
|
||||
views[eye].pose.orientation = frame.xr_frame.views[eye].pose.orientation;
|
||||
views[eye].pose.position = position_valid
|
||||
? frame.xr_frame.views[eye].pose.position
|
||||
: XrVector3f{0.0f, 0.0f, 0.0f};
|
||||
views[eye].pose.position = frame.xr_frame.views[eye].pose.position;
|
||||
views[eye].fov = frame.xr_frame.views[eye].fov;
|
||||
views[eye].subImage.swapchain = eye_swapchains_[eye].handle;
|
||||
views[eye].subImage.swapchain = retained_swapchains_[eye].handle;
|
||||
views[eye].subImage.imageRect = {
|
||||
{0, 0},
|
||||
{static_cast<int32_t>(eye_swapchains_[eye].width),
|
||||
static_cast<int32_t>(eye_swapchains_[eye].height)}};
|
||||
{static_cast<int32_t>(retained_swapchains_[eye].width),
|
||||
static_cast<int32_t>(retained_swapchains_[eye].height)}};
|
||||
views[eye].subImage.imageArrayIndex = 0;
|
||||
}
|
||||
XrCompositionLayerProjection projection{XR_TYPE_COMPOSITION_LAYER_PROJECTION};
|
||||
@@ -452,21 +513,8 @@ public:
|
||||
projection.views = views.data();
|
||||
const XrCompositionLayerBaseHeader* layers[] = {
|
||||
reinterpret_cast<const XrCompositionLayerBaseHeader*>(&projection)};
|
||||
end_ok = runtime_->EndFrame(frame.xr_frame, layers, 1);
|
||||
return runtime_->EndFrame(active_frame_, layers, 1);
|
||||
}
|
||||
|
||||
frame_active_ = false;
|
||||
active_frame_serial_ = 0;
|
||||
active_frame_ = {};
|
||||
frame.expects_gpu_submission = false;
|
||||
{
|
||||
std::lock_guard lock(submission_mutex_);
|
||||
awaiting_token_ = 0;
|
||||
submission_arrived_ = false;
|
||||
submission_success_ = false;
|
||||
submission_unsafe_ = false;
|
||||
}
|
||||
return release_ok && end_ok;
|
||||
}
|
||||
|
||||
bool Shutdown() {
|
||||
@@ -552,9 +600,13 @@ private:
|
||||
}
|
||||
|
||||
bool CreateSwapchains() {
|
||||
return CreateSwapchainPair(eye_swapchains_) && CreateSwapchainPair(retained_swapchains_);
|
||||
}
|
||||
|
||||
bool CreateSwapchainPair(std::array<EyeSwapchain, kOpenXREyeCount>& pair) {
|
||||
for (uint32_t eye = 0; eye < kOpenXREyeCount; ++eye) {
|
||||
const auto& view = runtime_->ViewConfiguration()[eye];
|
||||
auto& swapchain = eye_swapchains_[eye];
|
||||
auto& swapchain = pair[eye];
|
||||
swapchain.width = view.render_width;
|
||||
swapchain.height = view.render_height;
|
||||
|
||||
@@ -685,7 +737,15 @@ private:
|
||||
}
|
||||
|
||||
void DestroySwapchains() {
|
||||
for (auto& swapchain : eye_swapchains_) {
|
||||
DestroySwapchainPair(eye_swapchains_);
|
||||
DestroySwapchainPair(retained_swapchains_);
|
||||
have_retained_frame_ = false;
|
||||
retained_frame_ = {};
|
||||
swapchain_format_ = DXGI_FORMAT_UNKNOWN;
|
||||
}
|
||||
|
||||
void DestroySwapchainPair(std::array<EyeSwapchain, kOpenXREyeCount>& pair) {
|
||||
for (auto& swapchain : pair) {
|
||||
if (swapchain.handle != XR_NULL_HANDLE && !swapchain.acquired) {
|
||||
xrDestroySwapchain(swapchain.handle);
|
||||
} else if (swapchain.acquired) {
|
||||
@@ -694,7 +754,6 @@ private:
|
||||
}
|
||||
swapchain = {};
|
||||
}
|
||||
swapchain_format_ = DXGI_FORMAT_UNKNOWN;
|
||||
}
|
||||
|
||||
void EndActiveFrameWithoutLayers(const OpenXRFrame& frame) {
|
||||
@@ -752,6 +811,11 @@ private:
|
||||
OpenXRLogCallback logger_;
|
||||
OpenXRD3D12GraphicsRequirements requirements_{};
|
||||
std::array<EyeSwapchain, kOpenXREyeCount> eye_swapchains_{};
|
||||
std::array<EyeSwapchain, kOpenXREyeCount> retained_swapchains_{};
|
||||
OpenXRD3D12Frame retained_frame_{};
|
||||
uint64_t retained_session_serial_ = 0;
|
||||
uint64_t retained_space_serial_ = 0;
|
||||
bool have_retained_frame_ = false;
|
||||
DXGI_FORMAT aurora_format_ = DXGI_FORMAT_UNKNOWN;
|
||||
DXGI_FORMAT swapchain_format_ = DXGI_FORMAT_UNKNOWN;
|
||||
std::string last_error_;
|
||||
@@ -766,6 +830,8 @@ private:
|
||||
bool shutting_down_ = false;
|
||||
|
||||
uint64_t active_frame_serial_ = 0;
|
||||
uint64_t render_session_serial_ = 0;
|
||||
uint64_t render_space_serial_ = 0;
|
||||
OpenXRFrame active_frame_{};
|
||||
bool requirements_queried_ = false;
|
||||
bool owns_session_ = false;
|
||||
@@ -806,6 +872,10 @@ bool OpenXRD3D12Backend::FinishFrame(OpenXRD3D12Frame& frame, bool submit_layer)
|
||||
return m_impl->FinishFrame(frame, submit_layer);
|
||||
}
|
||||
|
||||
bool OpenXRD3D12Backend::RepeatFrame(const OpenXRD3D12Frame& frame) {
|
||||
return m_impl->RepeatFrame(frame);
|
||||
}
|
||||
|
||||
bool OpenXRD3D12Backend::Shutdown() { return m_impl->Shutdown(); }
|
||||
|
||||
bool OpenXRD3D12Backend::IsBound() const { return m_impl->IsBound(); }
|
||||
|
||||
@@ -365,6 +365,10 @@ public:
|
||||
recenter_requested_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
void SetEagerFrameHeartbeat(bool enabled) noexcept {
|
||||
eager_frame_heartbeat_.store(enabled, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void SetLeanBackDegrees(float degrees) noexcept {
|
||||
lean_back_degrees_.store(
|
||||
std::clamp(degrees, -RuntimeConfigFile::kVrLeanBackDegreesLimit,
|
||||
@@ -533,28 +537,33 @@ private:
|
||||
|
||||
OpenXRD3D12SubmissionStatus submission = OpenXRD3D12SubmissionStatus::Timeout;
|
||||
bool canceled_before_encode = false;
|
||||
bool submission_stalled = false;
|
||||
const auto submission_deadline =
|
||||
std::chrono::steady_clock::now() + std::chrono::milliseconds(250);
|
||||
const auto cancel_after =
|
||||
std::chrono::steady_clock::now() + std::chrono::milliseconds(50);
|
||||
while (!stop_.load(std::memory_order_acquire) &&
|
||||
submission == OpenXRD3D12SubmissionStatus::Timeout) {
|
||||
submission = backend_->WaitForSubmission(frame, 50);
|
||||
// Eager mode fills missed headset slots. With it off, completion
|
||||
// wakes us immediately at the game's cadence, while a 50 ms
|
||||
// keep-alive still protects pauses and window drags from black.
|
||||
// Read each iteration so the toggle also works during a stall.
|
||||
const auto wait_ms = eager_frame_heartbeat_.load(std::memory_order_relaxed)
|
||||
? static_cast<uint32_t>(std::clamp<XrDuration>(
|
||||
frame.xr_frame.predicted_display_period / 1'000'000 - 4, 0, 12))
|
||||
: 50u;
|
||||
submission = backend_->WaitForSubmission(frame, wait_ms);
|
||||
if (submission == OpenXRD3D12SubmissionStatus::Timeout) {
|
||||
// A pause, minimized window, or guest stall may leave no GX
|
||||
// frame to consume this packet. Withdraw it, then cancel the
|
||||
// matching bridge target only if Encode has not taken ownership.
|
||||
WithdrawPublishedFrame();
|
||||
canceled_before_encode = backend_->TryCancelPendingFrame(frame);
|
||||
if (canceled_before_encode) {
|
||||
break;
|
||||
if (std::chrono::steady_clock::now() >= cancel_after) {
|
||||
WithdrawPublishedFrame();
|
||||
canceled_before_encode = backend_->TryCancelPendingFrame(frame);
|
||||
if (canceled_before_encode) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (std::chrono::steady_clock::now() >= submission_deadline) {
|
||||
// Cancellation may lose a race to completion. Recheck
|
||||
// the callback-published predicate under the backend
|
||||
// mutex before declaring a stall at the deadline.
|
||||
submission = backend_->WaitForSubmission(frame, 0);
|
||||
submission_stalled =
|
||||
submission == OpenXRD3D12SubmissionStatus::Timeout;
|
||||
if (!backend_->RepeatFrame(frame)) {
|
||||
SetError(backend_->LastError());
|
||||
fatal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -573,10 +582,7 @@ private:
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (submission_stalled) {
|
||||
SetError("Aurora did not complete the OpenXR stereo submission; "
|
||||
"requesting a safe desktop fallback");
|
||||
fatal = true;
|
||||
if (fatal) {
|
||||
break;
|
||||
}
|
||||
const bool submit = submission == OpenXRD3D12SubmissionStatus::Success;
|
||||
@@ -723,6 +729,7 @@ private:
|
||||
std::atomic_bool teardown_requested_{false};
|
||||
std::atomic_bool recenter_requested_{false};
|
||||
std::atomic<float> lean_back_degrees_{RuntimeConfigFile::VrLeanBackDegrees()};
|
||||
std::atomic_bool eager_frame_heartbeat_{RuntimeConfigFile::VrEagerFrameHeartbeat()};
|
||||
std::atomic<PublishedFrame*> published_{nullptr};
|
||||
PublishedFrame published_frame_{};
|
||||
std::mutex published_mutex_;
|
||||
@@ -811,6 +818,14 @@ void OpenXRSetLeanBackDegrees(float degrees) noexcept {
|
||||
#endif
|
||||
}
|
||||
|
||||
void OpenXRSetEagerFrameHeartbeat(bool enabled) noexcept {
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
|
||||
OpenXRIntegration::Get().SetEagerFrameHeartbeat(enabled);
|
||||
#else
|
||||
(void)enabled;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string OpenXRLastError() {
|
||||
#if !defined(MKW_ENABLE_OPENXR)
|
||||
return "this build was compiled without OpenXR support";
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Exercise the real backend against a deterministic compositor and Aurora sink.
|
||||
// No headset, graphics driver, OpenXR loader, or translated game is required.
|
||||
#define CINTERFACE
|
||||
#define XR_USE_GRAPHICS_API_D3D12
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <d3d12.h>
|
||||
#include <openxr/openxr_platform.h>
|
||||
#include <aurora/d3d12_interop.h>
|
||||
#include "vr/openxr_d3d12.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
using namespace mkw::vr;
|
||||
|
||||
namespace {
|
||||
void RequireAt(bool condition, int line, const char* expression) {
|
||||
if (!condition) {
|
||||
std::cerr << "OpenXR replay test failed at line " << line << ": " << expression << '\n';
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
#define Require(condition) RequireAt((condition), __LINE__, #condition)
|
||||
struct Image { uint64_t content = 0; };
|
||||
struct Swapchain {
|
||||
Image image;
|
||||
bool acquired = false;
|
||||
bool waited = false;
|
||||
bool released = false;
|
||||
};
|
||||
std::vector<AuroraD3D12StereoTarget> targets;
|
||||
AuroraD3D12StereoSubmittedCallback callback = nullptr;
|
||||
void* callback_data = nullptr;
|
||||
uint64_t pending_token = 0;
|
||||
bool encoded = false;
|
||||
bool should_render = true;
|
||||
bool tracking_valid = true;
|
||||
bool change_space = false;
|
||||
bool restart_session = false;
|
||||
bool stop_session = false;
|
||||
bool fail_end = false;
|
||||
uint64_t displayed_content = 0;
|
||||
uint32_t layer_count = 0;
|
||||
uint32_t releases = 0;
|
||||
uint32_t live_swapchains = 0;
|
||||
XrTime display_time = 0;
|
||||
XrStructureType layer_type = XR_TYPE_UNKNOWN;
|
||||
XrPosef quad_pose{};
|
||||
|
||||
void Complete(bool success = true) {
|
||||
Require(pending_token != 0);
|
||||
if (success) {
|
||||
for (const auto& target : targets)
|
||||
reinterpret_cast<Image*>(target.resource)->content = pending_token;
|
||||
}
|
||||
callback(pending_token, success, callback_data);
|
||||
pending_token = 0;
|
||||
encoded = false;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE FeatureSupport(ID3D12Device*, D3D12_FEATURE,
|
||||
void* data, UINT) {
|
||||
static_cast<D3D12_FEATURE_DATA_FEATURE_LEVELS*>(data)->MaxSupportedFeatureLevel =
|
||||
D3D_FEATURE_LEVEL_11_0;
|
||||
return S_OK;
|
||||
}
|
||||
XrResult XRAPI_CALL Requirements(XrInstance, XrSystemId,
|
||||
XrGraphicsRequirementsD3D12KHR* requirements) {
|
||||
requirements->adapterLuid = {};
|
||||
requirements->minFeatureLevel = D3D_FEATURE_LEVEL_11_0;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
// A COM vtable in its C representation supplies the single device operation
|
||||
// used by BindAurora. The production backend is compiled normally as C++.
|
||||
bool aurora_d3d12_get_native_handles(AuroraD3D12NativeHandles* handles) {
|
||||
static ID3D12DeviceVtbl vtable{};
|
||||
vtable.CheckFeatureSupport = FeatureSupport;
|
||||
static ID3D12Device device{&vtable};
|
||||
*handles = {&device, &device, DXGI_FORMAT_R8G8B8A8_UNORM, 0, 0};
|
||||
return true;
|
||||
}
|
||||
bool aurora_d3d12_enable_stereo_bridge(AuroraD3D12StereoSubmittedCallback cb, void* data) {
|
||||
callback = cb;
|
||||
callback_data = data;
|
||||
return true;
|
||||
}
|
||||
bool aurora_d3d12_set_stereo_targets(uint64_t token, const AuroraD3D12StereoTarget* data,
|
||||
uint32_t count) {
|
||||
Require(pending_token == 0);
|
||||
pending_token = token;
|
||||
targets.assign(data, data + count);
|
||||
return true;
|
||||
}
|
||||
bool aurora_d3d12_cancel_stereo_targets(uint64_t token) {
|
||||
if (encoded || token != pending_token) return false;
|
||||
pending_token = 0;
|
||||
return true;
|
||||
}
|
||||
bool aurora_d3d12_disable_stereo_bridge() {
|
||||
pending_token = 0;
|
||||
encoded = false;
|
||||
return true; // Simulate a successful queue drain.
|
||||
}
|
||||
|
||||
XrResult XRAPI_CALL xrCreateSwapchain(XrSession, const XrSwapchainCreateInfo*, XrSwapchain* out) {
|
||||
*out = reinterpret_cast<XrSwapchain>(new Swapchain);
|
||||
++live_swapchains;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
XrResult XRAPI_CALL xrEnumerateSwapchainImages(XrSwapchain handle, uint32_t capacity,
|
||||
uint32_t* count, XrSwapchainImageBaseHeader* images) {
|
||||
*count = 1; // Single-image swapchains also require a separate retained pair.
|
||||
if (capacity) reinterpret_cast<XrSwapchainImageD3D12KHR*>(images)->texture =
|
||||
reinterpret_cast<ID3D12Resource*>(&reinterpret_cast<Swapchain*>(handle)->image);
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
XrResult XRAPI_CALL xrAcquireSwapchainImage(XrSwapchain handle,
|
||||
const XrSwapchainImageAcquireInfo*, uint32_t* index) {
|
||||
auto& chain = *reinterpret_cast<Swapchain*>(handle);
|
||||
Require(!chain.acquired);
|
||||
chain.acquired = true;
|
||||
chain.waited = false;
|
||||
*index = 0;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
XrResult XRAPI_CALL xrWaitSwapchainImage(XrSwapchain handle, const XrSwapchainImageWaitInfo*) {
|
||||
auto& chain = *reinterpret_cast<Swapchain*>(handle);
|
||||
Require(chain.acquired);
|
||||
chain.waited = true;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
XrResult XRAPI_CALL xrReleaseSwapchainImage(XrSwapchain handle,
|
||||
const XrSwapchainImageReleaseInfo*) {
|
||||
auto& chain = *reinterpret_cast<Swapchain*>(handle);
|
||||
Require(chain.acquired && chain.waited);
|
||||
chain.acquired = false;
|
||||
chain.released = true;
|
||||
++releases;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
XrResult XRAPI_CALL xrDestroySwapchain(XrSwapchain handle) {
|
||||
auto* chain = reinterpret_cast<Swapchain*>(handle);
|
||||
Require(!chain->acquired);
|
||||
delete chain;
|
||||
--live_swapchains;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
||||
namespace mkw::vr {
|
||||
OpenXRRuntime::OpenXRRuntime(OpenXRLogCallback) {
|
||||
m_instance = reinterpret_cast<XrInstance>(this);
|
||||
m_swapchain_formats = {DXGI_FORMAT_R8G8B8A8_UNORM_SRGB};
|
||||
for (auto& view : m_view_configuration) {
|
||||
view.render_width = 100;
|
||||
view.render_height = 80;
|
||||
}
|
||||
}
|
||||
OpenXRRuntime::~OpenXRRuntime() = default;
|
||||
bool OpenXRRuntime::GetInstanceProcAddress(const char*, PFN_xrVoidFunction* out) {
|
||||
*out = reinterpret_cast<PFN_xrVoidFunction>(Requirements);
|
||||
return true;
|
||||
}
|
||||
bool OpenXRRuntime::CreateSession(const void*) {
|
||||
m_session = reinterpret_cast<XrSession>(this);
|
||||
m_session_running = true;
|
||||
++m_session_run_serial;
|
||||
return true;
|
||||
}
|
||||
void OpenXRRuntime::DestroySession() { m_session_running = false; }
|
||||
void OpenXRRuntime::ObserveResult(XrResult) noexcept {}
|
||||
OpenXREventStatus OpenXRRuntime::PollEvents() {
|
||||
if (change_space) { ++m_reference_space_change.serial; change_space = false; }
|
||||
if (restart_session) { ++m_session_run_serial; restart_session = false; }
|
||||
if (stop_session) { m_session_running = false; stop_session = false; }
|
||||
return OpenXREventStatus::Continue;
|
||||
}
|
||||
OpenXRFrameStatus OpenXRRuntime::WaitFrame(OpenXRFrame& frame) {
|
||||
Require(m_frame_phase == FramePhase::Idle);
|
||||
frame = {};
|
||||
frame.serial = m_next_frame_serial++;
|
||||
frame.predicted_display_time = frame.serial * 11'111'111;
|
||||
frame.predicted_display_period = 11'111'111;
|
||||
frame.should_render = should_render;
|
||||
m_active_frame_serial = frame.serial;
|
||||
m_frame_phase = FramePhase::Waited;
|
||||
return OpenXRFrameStatus::Ready;
|
||||
}
|
||||
bool OpenXRRuntime::BeginFrame(const OpenXRFrame& frame) {
|
||||
Require(m_frame_phase == FramePhase::Waited && frame.serial == m_active_frame_serial);
|
||||
m_frame_phase = FramePhase::Begun;
|
||||
return true;
|
||||
}
|
||||
bool OpenXRRuntime::LocateViews(OpenXRFrame& frame) {
|
||||
frame.views_valid = tracking_valid;
|
||||
frame.view_state_flags = XR_VIEW_STATE_POSITION_VALID_BIT | XR_VIEW_STATE_ORIENTATION_VALID_BIT;
|
||||
for (auto& view : frame.views) {
|
||||
view.pose.orientation.w = 1;
|
||||
view.pose.position.x = static_cast<float>(frame.serial);
|
||||
view.fov.angleLeft = -0.75f;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool OpenXRRuntime::EndFrame(const OpenXRFrame& frame,
|
||||
const XrCompositionLayerBaseHeader* const* layers, uint32_t count) {
|
||||
Require(m_frame_phase == FramePhase::Begun && frame.serial == m_active_frame_serial);
|
||||
Require(frame.predicted_display_time > display_time);
|
||||
display_time = frame.predicted_display_time;
|
||||
layer_count = count;
|
||||
if (count) {
|
||||
Require(frame.should_render && count == 1);
|
||||
layer_type = layers[0]->type;
|
||||
const auto check_image = [](const XrSwapchainSubImage& subimage) {
|
||||
const auto& chain = *reinterpret_cast<Swapchain*>(subimage.swapchain);
|
||||
Require(chain.released && !chain.acquired && chain.image.content != 0);
|
||||
return chain.image.content;
|
||||
};
|
||||
if (layer_type == XR_TYPE_COMPOSITION_LAYER_PROJECTION) {
|
||||
auto& projection = *reinterpret_cast<const XrCompositionLayerProjection*>(layers[0]);
|
||||
Require(projection.viewCount == 2);
|
||||
for (const auto& view : {projection.views[0], projection.views[1]}) {
|
||||
displayed_content = check_image(view.subImage);
|
||||
// Detect a new image paired with an old pose, or a repeated image
|
||||
// falsely labelled with the latest compositor pose.
|
||||
Require(view.pose.position.x == static_cast<float>(displayed_content));
|
||||
Require(view.fov.angleLeft == -0.75f);
|
||||
}
|
||||
} else {
|
||||
Require(layer_type == XR_TYPE_COMPOSITION_LAYER_QUAD);
|
||||
const auto& quad = *reinterpret_cast<const XrCompositionLayerQuad*>(layers[0]);
|
||||
displayed_content = check_image(quad.subImage);
|
||||
quad_pose = quad.pose;
|
||||
}
|
||||
}
|
||||
m_frame_phase = FramePhase::Idle;
|
||||
if (fail_end) {
|
||||
fail_end = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool OpenXRRuntime::EndFrameWithoutLayers(const OpenXRFrame& frame) {
|
||||
return EndFrame(frame, nullptr, 0);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
OpenXRRuntime runtime;
|
||||
OpenXRD3D12Backend backend;
|
||||
Require(backend.QueryGraphicsRequirements(runtime) && backend.BindAurora(runtime));
|
||||
Require(live_swapchains == 4);
|
||||
OpenXRD3D12Presentation presentation;
|
||||
OpenXRD3D12Frame frame;
|
||||
const auto begin = [&] {
|
||||
Require(backend.BeginFrame(presentation, frame) == OpenXRD3D12BeginStatus::Ready);
|
||||
};
|
||||
const auto finish = [&] {
|
||||
Complete();
|
||||
Require(backend.WaitForSubmission(frame, 0) == OpenXRD3D12SubmissionStatus::Success);
|
||||
Require(backend.FinishFrame(frame, true));
|
||||
};
|
||||
begin();
|
||||
Require(backend.RepeatFrame(frame) && layer_count == 0); // No valid image yet.
|
||||
finish();
|
||||
const auto first = displayed_content;
|
||||
begin();
|
||||
encoded = true;
|
||||
const auto releases_before_stall = releases;
|
||||
for (int i = 0; i < 300; ++i) {
|
||||
Require(backend.WaitForSubmission(frame, 0) == OpenXRD3D12SubmissionStatus::Timeout);
|
||||
Require(!backend.TryCancelPendingFrame(frame));
|
||||
Require(backend.RepeatFrame(frame));
|
||||
Require(layer_count == 1 && displayed_content == first);
|
||||
Require(releases == releases_before_stall);
|
||||
}
|
||||
const auto second = frame.xr_frame.serial;
|
||||
finish(); // A late completion keeps its original render token and poses.
|
||||
Require(displayed_content == second);
|
||||
begin();
|
||||
Require(backend.TryCancelPendingFrame(frame));
|
||||
Require(backend.FinishFrame(frame, false));
|
||||
Require(layer_count == 1 && displayed_content == second);
|
||||
|
||||
should_render = false;
|
||||
begin();
|
||||
Require(!frame.expects_gpu_submission && backend.FinishFrame(frame, false));
|
||||
Require(layer_count == 0);
|
||||
should_render = true;
|
||||
tracking_valid = false;
|
||||
begin();
|
||||
Require(!frame.expects_gpu_submission && backend.FinishFrame(frame, false));
|
||||
Require(layer_count == 1 && displayed_content == second);
|
||||
tracking_valid = true;
|
||||
|
||||
presentation.mode = OpenXRD3D12FrameMode::VirtualScreen;
|
||||
presentation.quad_anchored = true;
|
||||
presentation.quad_pose.position.z = -3;
|
||||
begin();
|
||||
Require(targets.size() == 1);
|
||||
finish();
|
||||
const auto menu = displayed_content;
|
||||
begin();
|
||||
Require(backend.RepeatFrame(frame));
|
||||
Require(displayed_content == menu && quad_pose.position.z == -3);
|
||||
Require(backend.TryCancelPendingFrame(frame) && backend.FinishFrame(frame, false));
|
||||
|
||||
presentation.mode = OpenXRD3D12FrameMode::ImmersiveProjection;
|
||||
begin();
|
||||
change_space = true;
|
||||
Require(backend.RepeatFrame(frame));
|
||||
Require(backend.RepeatFrame(frame) && layer_count == 0);
|
||||
finish(); // A render from before the space change must remain invalid.
|
||||
Require(layer_count == 0);
|
||||
begin();
|
||||
finish();
|
||||
Require(layer_count == 1);
|
||||
restart_session = true;
|
||||
runtime.PollEvents();
|
||||
begin();
|
||||
Require(backend.RepeatFrame(frame) && layer_count == 0);
|
||||
finish();
|
||||
Require(layer_count == 1);
|
||||
|
||||
begin();
|
||||
const auto before_failure = releases;
|
||||
Complete(false);
|
||||
Require(backend.WaitForSubmission(frame, 0) == OpenXRD3D12SubmissionStatus::Failed);
|
||||
Require(!backend.FinishFrame(frame, false));
|
||||
Require(releases == before_failure); // Never release possibly in-flight GPU work.
|
||||
Require(backend.Shutdown() && live_swapchains == 0);
|
||||
|
||||
// A runtime stop or failed xrEndFrame during a stall must still drain the
|
||||
// pending target without trying to end an already consumed frame token.
|
||||
for (bool fail_submission : {false, true}) {
|
||||
display_time = 0;
|
||||
OpenXRRuntime next_runtime;
|
||||
OpenXRD3D12Backend next_backend;
|
||||
Require(next_backend.QueryGraphicsRequirements(next_runtime));
|
||||
Require(next_backend.BindAurora(next_runtime));
|
||||
Require(next_backend.BeginFrame(presentation, frame) == OpenXRD3D12BeginStatus::Ready);
|
||||
encoded = true;
|
||||
stop_session = !fail_submission;
|
||||
fail_end = fail_submission;
|
||||
Require(!next_backend.RepeatFrame(frame));
|
||||
Require(next_backend.Shutdown() && live_swapchains == 0);
|
||||
}
|
||||
std::cout << "OpenXR retained-frame tests passed\n";
|
||||
}
|
||||
Reference in new issue
Block a user