Anchored Main Menu Quad

This commit is contained in:
iChris4 committed 2026-09-08 21:24:37 +02:00
1 parent 913eda2b3a
commit bd106dd1fe
6 files changed
+93 -29

No files matched your search

+5 -5
View File
@@ -386,11 +386,11 @@ inline void EnsureConfigFile() {
"# model of your kart, the vehicle included.\n"
"first_person_hidden_model = 0\n"
"# Keyboard shortcut that recenters the VR view, naming the key the\n"
"# way SDL does (F9, Home, Keypad 5, ...). It moves the view to where\n"
"# you are sitting now and nothing else: forward and the horizon come\n"
"# from the headset's own reference space, so recentering cannot tilt\n"
"# the game. Applies during an immersive race; the menu screen is\n"
"# head-locked already. Rebindable from the F10 menu under VR. Leave\n"
"# way SDL does (F9, Home, Keypad 5, ...). It moves the race view to\n"
"# where you are sitting now and brings the menu screen back upright in\n"
"# front of you. The race view moves in position only: forward and the\n"
"# horizon come from the headset's own reference space, so recentering\n"
"# cannot tilt the game. Rebindable from the F10 menu under VR. Leave\n"
"# empty to unbind.\n"
"recenter_key = \"F9\"\n"
"# Fixed pitch of the game camera, in degrees, for a player sitting\n"
+12 -4
View File
@@ -41,10 +41,17 @@ struct OpenXRD3D12GraphicsRequirements {
struct OpenXRD3D12Presentation {
OpenXRD3D12FrameMode mode = OpenXRD3D12FrameMode::ImmersiveProjection;
// Used only by VirtualScreen. The quad is head-locked in XR_VIEW_SPACE
// and centered straight ahead at -Z.
// Used only by VirtualScreen.
float quad_distance_meters = 2.0f;
float quad_width_meters = 2.4f;
// When quad_anchored is set, the quad is placed at quad_pose in the
// application reference space and stays put as the player looks around.
// Otherwise it falls back to being head-locked in XR_VIEW_SPACE, centered
// straight ahead at -Z, which is what happens until tracking has produced a
// head pose good enough to anchor against.
bool quad_anchored = false;
XrPosef quad_pose{{0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}};
};
struct OpenXRD3D12Frame {
@@ -96,8 +103,9 @@ public:
// Releases acquired images and calls xrEndFrame. submit_layer must only be
// true after WaitForSubmission returned Success. Immersive frames submit
// XrCompositionLayerProjection; virtual-screen frames submit a head-locked
// XrCompositionLayerQuad using the single mono target.
// XrCompositionLayerProjection; virtual-screen frames submit an
// XrCompositionLayerQuad using the single mono target, placed as the
// presentation's quad_anchored/quad_pose describe.
bool FinishFrame(OpenXRD3D12Frame& frame, bool submit_layer);
// Call on the XR owner thread after Aurora's worker is idle and before
+7 -6
View File
@@ -36,12 +36,13 @@ void OpenXRServiceProducerFrameBoundary() noexcept;
bool OpenXRIsRunning() noexcept;
std::string OpenXRLastError();
// Recenters the view on where the player is sitting now. Position only: the
// forward direction and the horizon come from the OpenXR reference space and
// are never relatched from the headset, so recentering cannot tilt the game's
// horizon. Use the runtime's own recenter gesture to change forward. Callable
// from any thread; serviced on the XR pacing thread and ignored outside an
// immersive race, where the head-locked menu screen has no origin to recenter.
// Recenters on where the player is sitting now: it moves the immersive race
// view's origin, and re-places the anchored menu screen upright in front of
// them. Position only for the race view: the forward direction and the horizon
// come from the OpenXR reference space and are never relatched from the
// headset, so recentering cannot tilt the game's horizon. Use the runtime's own
// recenter gesture to change forward. Callable from any thread; serviced once
// per frame on the XR pacing thread.
void OpenXRRequestRecenter() noexcept;
// Sets a fixed pitch of the game camera for a player sitting reclined, in
+4 -4
View File
@@ -885,10 +885,10 @@ void DrawVrSettings() {
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Makes where you are sitting right now the centre of the view. Position "
"only, so the horizon stays level and forward is unchanged; use your "
"headset's own recenter to change forward. Applies during an immersive "
"race, as the menu screen already follows your head.");
"Makes where you are sitting right now the centre of the view, and brings "
"the menu screen back upright in front of you. The race view moves in "
"position only, so the horizon stays level and forward is unchanged; use "
"your headset's own recenter to change forward.");
}
ImGui::SameLine();
// Click to arm, then the next key press is captured in HandleEvents.
+12 -2
View File
@@ -404,15 +404,25 @@ public:
} else if (frame.presentation.mode == OpenXRD3D12FrameMode::VirtualScreen) {
XrCompositionLayerQuad quad{XR_TYPE_COMPOSITION_LAYER_QUAD};
quad.layerFlags = 0;
quad.space = runtime_->ViewSpace();
quad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
quad.subImage.swapchain = eye_swapchains_[0].handle;
quad.subImage.imageRect = {{0, 0},
{static_cast<int32_t>(eye_swapchains_[0].width),
static_cast<int32_t>(eye_swapchains_[0].height)}};
quad.subImage.imageArrayIndex = 0;
if (frame.presentation.quad_anchored) {
// Placed in the application space, so the screen keeps its place
// in the room while the player looks around it.
quad.space = runtime_->AppSpace();
quad.pose = frame.presentation.quad_pose;
} else {
// No head pose to anchor against yet: keep it in front of the
// player so the menus are never left stranded behind them.
quad.space = runtime_->ViewSpace();
quad.pose.orientation = {0.0f, 0.0f, 0.0f, 1.0f};
quad.pose.position = {0.0f, 0.0f, -std::max(0.25f, frame.presentation.quad_distance_meters)};
quad.pose.position = {
0.0f, 0.0f, -std::max(0.25f, frame.presentation.quad_distance_meters)};
}
quad.size.width = std::max(0.25f, frame.presentation.quad_width_meters);
quad.size.height = quad.size.width * static_cast<float>(eye_swapchains_[0].height) /
static_cast<float>(eye_swapchains_[0].width);
+53 -8
View File
@@ -135,6 +135,21 @@ std::array<float, 3> CenterPosition(const OpenXRFrame& frame) noexcept {
};
}
// Places an upright screen `distance` metres ahead of the head. Only the
// head's yaw is used, so the screen is never pitched or rolled by whatever the
// player's head happened to be doing when it was anchored.
XrPosef ScreenPoseAhead(const OpenXRFrame& frame, float distance) noexcept {
const auto& q = frame.views[0].pose.orientation;
const float yaw =
std::atan2(2.0f * (q.x * q.z + q.w * q.y), 1.0f - 2.0f * (q.x * q.x + q.y * q.y));
const std::array<float, 3> center = CenterPosition(frame);
XrPosef pose{};
pose.orientation = {0.0f, std::sin(yaw * 0.5f), 0.0f, std::cos(yaw * 0.5f)};
pose.position = {center[0] - std::sin(yaw) * distance, center[1],
center[2] - std::cos(yaw) * distance};
return pose;
}
void IdentityEye(AuroraStereoEye& eye) noexcept {
std::fill(std::begin(eye.projection), std::end(eye.projection), 0.0f);
eye.projection[0] = 1.0f;
@@ -492,6 +507,11 @@ private:
break;
}
// Both of these read this frame's located head pose and must run
// before FinishFrame submits a layer built from it.
ServiceRecenterRequest();
UpdateVirtualScreenPose(frame);
if (!frame.expects_gpu_submission) {
if (!backend_->FinishFrame(frame, false)) {
SetError(backend_->LastError());
@@ -603,20 +623,12 @@ private:
IdentityEye(destination.eyes[eye]);
}
if (!immersive) {
// The virtual screen is submitted in view space, so it is
// head-locked and has no origin to recenter. Drop the request rather
// than leaving it queued for a race start that latches the position
// on its own anyway.
recenter_requested_.store(false, std::memory_order_relaxed);
last_immersive_ = false;
return;
}
const bool position_valid =
(source.xr_frame.view_state_flags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0;
if (recenter_requested_.exchange(false, std::memory_order_acq_rel)) {
ResetTrackingOrigin();
}
// Latch on the first immersive frame, after a recenter or an origin
// change, and on re-entry from the virtual screen so a race start
// recenters a player who shifted during the menus. Position only: the
@@ -640,6 +652,34 @@ private:
}
}
// Runs once per located frame, before the virtual screen is placed and
// before the immersive origin is latched, so a recenter reaches both from
// this frame's head pose rather than the next one's.
void ServiceRecenterRequest() noexcept {
if (recenter_requested_.exchange(false, std::memory_order_acq_rel)) {
ResetTrackingOrigin();
}
}
// Anchors the menu screen in the application space and holds it there. The
// pose is captured once, from the first frame whose head pose is good enough
// to place it, and released again by a recenter or an origin change.
void UpdateVirtualScreenPose(OpenXRD3D12Frame& frame) noexcept {
if (frame.presentation.mode != OpenXRD3D12FrameMode::VirtualScreen) {
return;
}
constexpr XrViewStateFlags kPoseUsable =
XR_VIEW_STATE_ORIENTATION_VALID_BIT | XR_VIEW_STATE_POSITION_VALID_BIT;
if (!virtual_screen_pose_valid_ && frame.xr_frame.views_valid &&
(frame.xr_frame.view_state_flags & kPoseUsable) == kPoseUsable) {
virtual_screen_pose_ = ScreenPoseAhead(
frame.xr_frame, std::max(0.25f, frame.presentation.quad_distance_meters));
virtual_screen_pose_valid_ = true;
}
frame.presentation.quad_anchored = virtual_screen_pose_valid_;
frame.presentation.quad_pose = virtual_screen_pose_;
}
void ApplyPendingReferenceSpaceChange(const OpenXRFrame& frame) noexcept {
if (runtime_->ConsumeAppSpaceChangesThrough(frame.predicted_display_time)) {
ResetTrackingOrigin();
@@ -650,6 +690,9 @@ private:
base_position_ = {};
base_position_valid_ = false;
last_immersive_ = false;
// The anchored menu screen is placed in the same space, so it is stale
// for exactly the same reasons and is re-placed on the next frame.
virtual_screen_pose_valid_ = false;
}
void WaitForStopOrDelay(std::chrono::milliseconds delay) {
@@ -689,6 +732,8 @@ private:
std::string last_error_;
std::array<float, 3> base_position_{};
bool base_position_valid_ = false;
XrPosef virtual_screen_pose_{{0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}};
bool virtual_screen_pose_valid_ = false;
bool last_immersive_ = false;
uint64_t applied_session_run_serial_ = 0;
bool session_was_active_ = false;