From 3d11ccbbf8d780de5d2fd6cc174102713308ee9c Mon Sep 17 00:00:00 2001 From: Pixelplanet Date: Tue, 20 Oct 2026 15:32:18 +0200 Subject: [PATCH] fix(keyboard): correct floor vs eye coordinate transform and eliminate persistent draft buffer --- src/app/app.cpp | 27 ++-- src/gfx/vulkan_renderer.hpp | 2 + src/xr/vr_keyboard.cpp | 250 ++++++++++++++++++++++-------------- src/xr/vr_keyboard.hpp | 13 +- src/xr/xr_context.cpp | 68 ++++++++++ src/xr/xr_context.hpp | 8 ++ 6 files changed, 253 insertions(+), 115 deletions(-) diff --git a/src/app/app.cpp b/src/app/app.cpp index 0dfaeeb..9eca793 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -1920,27 +1920,30 @@ int App::run() { xr_.triggerHaptic(0, 0.025f, 0.0f, 0.5f); xr_.triggerHaptic(1, 0.025f, 0.0f, 0.5f); + auto openKeyboardForField = [&](SmbModalState::Field field, const std::string& desc, const std::string& currentVal, bool isPass) { + smbModal_.editingField = field; + float stagePos[3]{0.0f, 1.65f, 0.0f}; + float stageFwd[3]{0.0f, 0.0f, -1.0f}; + const float* fwd = renderer_.screenForward(); + xr_.transformToStage(in.headPos, fwd, stagePos, stageFwd); + VrKeyboard::instance().show(desc, currentVal, isPass, 256, in.headPos, fwd, stagePos, stageFwd); + }; + if (chosen == 0) { - smbModal_.editingField = SmbModalState::Field::Host; - VrKeyboard::instance().show("SMB Server IP / Hostname", smbModal_.host, false, 256, in.headPos, in.headForward); + openKeyboardForField(SmbModalState::Field::Host, "SMB Server IP / Hostname", smbModal_.host, false); } else if (chosen == 1) { - smbModal_.editingField = SmbModalState::Field::Share; - VrKeyboard::instance().show("SMB Share Name (empty for all)", smbModal_.share, false, 256, in.headPos, in.headForward); + openKeyboardForField(SmbModalState::Field::Share, "SMB Share Name (empty for all)", smbModal_.share, false); } else if (chosen == 2) { smbModal_.anon = !smbModal_.anon; updateSmbModalMenu(); } else if (chosen == 3) { - smbModal_.editingField = SmbModalState::Field::User; - VrKeyboard::instance().show("SMB Username", smbModal_.user, false, 256, in.headPos, in.headForward); + openKeyboardForField(SmbModalState::Field::User, "SMB Username", smbModal_.user, false); } else if (chosen == 4) { - smbModal_.editingField = SmbModalState::Field::Pass; - VrKeyboard::instance().show("SMB Password", smbModal_.pass, true, 256, in.headPos, in.headForward); + openKeyboardForField(SmbModalState::Field::Pass, "SMB Password", smbModal_.pass, true); } else if (chosen == 5) { - smbModal_.editingField = SmbModalState::Field::Workgroup; - VrKeyboard::instance().show("SMB Workgroup / Domain", smbModal_.workgroup, false, 256, in.headPos, in.headForward); + openKeyboardForField(SmbModalState::Field::Workgroup, "SMB Workgroup / Domain", smbModal_.workgroup, false); } else if (chosen == 6) { - smbModal_.editingField = SmbModalState::Field::Label; - VrKeyboard::instance().show("SMB Nickname", smbModal_.label, false, 256, in.headPos, in.headForward); + openKeyboardForField(SmbModalState::Field::Label, "SMB Nickname", smbModal_.label, false); } else if (chosen == 7) { // Save SMB Share saveSmbModal(); diff --git a/src/gfx/vulkan_renderer.hpp b/src/gfx/vulkan_renderer.hpp index 58755e2..55f9d4e 100644 --- a/src/gfx/vulkan_renderer.hpp +++ b/src/gfx/vulkan_renderer.hpp @@ -96,6 +96,8 @@ public: // Repositions the flat screen in front of the given head pose (headForward is // a world-space direction); resets the viewing distance to the default. void recenterScreen(const float headPos[3], const float headForward[3]); + const float* screenPivot() const { return screenPivot_; } + const float* screenForward() const { return screenForward_; } // Moves the flat screen farther (positive) or nearer (negative), clamped. void adjustScreenDistance(float delta); // Parametric curvature for cinema screen: 0.0 = flat plane, 1.0 = full cylinder wrap. diff --git a/src/xr/vr_keyboard.cpp b/src/xr/vr_keyboard.cpp index dc5a646..1c8ccc8 100644 --- a/src/xr/vr_keyboard.cpp +++ b/src/xr/vr_keyboard.cpp @@ -2,6 +2,7 @@ #include "util/logging.hpp" #include #include +#include #if defined(__linux__) || defined(__APPLE__) #include @@ -23,6 +24,8 @@ Fn_VR_InitInternal g_VR_InitInternal = nullptr; Fn_VR_ShutdownInternal g_VR_ShutdownInternal = nullptr; Fn_VR_GetGenericInterface g_VR_GetGenericInterface = nullptr; +static uint64_t sKeyboardNonce = 1000; + } // namespace VrKeyboard& VrKeyboard::instance() { @@ -138,6 +141,7 @@ void VrKeyboard::shutdown() { isOpen_ = false; doneSubmitted_ = false; kbConfigured_ = false; + currentNonce_ = 0; } bool VrKeyboard::configureKeyboardOverlay() { @@ -148,7 +152,7 @@ bool VrKeyboard::configureKeyboardOverlay() { vr::HmdMatrix34_t mat{}; for (int r = 0; r < 3; ++r) { for (int c = 0; c < 4; ++c) { - mat.m[r][c] = kbMat_[r][c]; + mat.m[r][c] = kbMatStage_[r][c]; } } @@ -160,8 +164,9 @@ bool VrKeyboard::configureKeyboardOverlay() { ov->SetOverlayFlag(h, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true); ov->ShowOverlay(h); foundAny = true; - PIXELVR_LOG_INFO("VrKeyboard: Configured keyboard overlay %llu ('%s') with interactive laser mouse and standing transform", - static_cast(h), name ? name : "unknown"); + PIXELVR_LOG_INFO("VrKeyboard: Configured keyboard overlay %llu ('%s') with interactive laser mouse at standing pos (%.2f, %.2f, %.2f)", + static_cast(h), name ? name : "unknown", + mat.m[0][3], mat.m[1][3], mat.m[2][3]); }; // 1. Check known specific overlay keys @@ -203,25 +208,28 @@ bool VrKeyboard::show(const std::string& description, const std::string& existingText, bool isPassword, int maxChars, - const float* headPos, - const float* headForward) { + const float* headPosLocal, + const float* headForwardLocal, + const float* headPosStage, + const float* headForwardStage) { if (!available_ && !init()) { return false; } - auto* sys = reinterpret_cast(vrSystem_); auto* ov = reinterpret_cast(vrOverlay_); if (!ov) return false; if (isOpen_) { hide(); } - // Explicitly hide any existing keyboard session in SteamVR to clear lingering text buffers + // Explicitly hide any existing keyboard session in SteamVR to clear lingering state ov->HideKeyboard(); + currentNonce_ = ++sKeyboardNonce; description_ = description; existingText_ = existingText; currentText_ = existingText; isPassword_ = isPassword; + maxChars_ = maxChars; doneSubmitted_ = false; userTyped_ = false; @@ -229,31 +237,18 @@ bool VrKeyboard::show(const std::string& description, ? vr::k_EGamepadTextInputModePassword : vr::k_EGamepadTextInputModeNormal; - // Calculate keyboard world matrix in TrackingUniverseStanding - float hx = 0.0f, hy = 1.6f, hz = 0.0f; + // ------------------------------------------------------------------------- + // 1. Calculate Local transform (for OpenXR laser pointer collision in VulkanRenderer) + // ------------------------------------------------------------------------- + float hx = 0.0f, hy = 0.0f, hz = 0.0f; float fx = 0.0f, fz = -1.0f; - - if (headPos && headForward) { - hx = headPos[0]; - hy = headPos[1]; - hz = headPos[2]; - fx = headForward[0]; - fz = headForward[2]; - } else if (sys) { - vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount]; - sys->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0.0f, poses, vr::k_unMaxTrackedDeviceCount); - if (poses[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid) { - const auto& hMat = poses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking; - hx = hMat.m[0][3]; - hy = hMat.m[1][3]; - hz = hMat.m[2][3]; - // OpenVR forward is -Z (col 2 of rotation matrix) - fx = -hMat.m[0][2]; - fz = -hMat.m[2][2]; - } + if (headPosLocal && headForwardLocal) { + hx = headPosLocal[0]; + hy = headPosLocal[1]; + hz = headPosLocal[2]; + fx = headForwardLocal[0]; + fz = headForwardLocal[2]; } - - // Flatten forward vector to horizontal plane float flen = std::sqrt(fx * fx + fz * fz); if (flen < 1e-3f) { fx = 0.0f; @@ -262,71 +257,121 @@ bool VrKeyboard::show(const std::string& description, fx /= flen; fz /= flen; } - const float yaw = std::atan2(-fx, -fz); + const float yawLocal = std::atan2(-fx, -fz); - // Place keyboard 1.05m in front of eyes, 0.20m below eye level. - // The modal dialog box above ends at ~hy + 0.03m; this places the top edge of the - // keyboard (half-height ~0.20m) right at hy - 0.004m, exactly 3.4cm below the modal dialog. - const float dist = 1.05f; - const float kbX = hx + fx * dist; - const float kbY = hy - 0.20f; - const float kbZ = hz + fz * dist; + // Keyboard placed 1.15m in front of eyes, 0.22m below eye level. + // The floating modal dialog box in VulkanRenderer ends at ~hy + 0.03m; this places the top + // of the keyboard (half-height 0.20m) at ~hy - 0.02m, creating an elegant ~5cm vertical gap + // directly beneath the modal dialog. + const float dist = 1.15f; + const float kbLocalX = hx + fx * dist; + const float kbLocalY = hy - 0.22f; + const float kbLocalZ = hz + fz * dist; - // Tilt upward toward eyes by ~11.5 degrees (-0.20 rad). - // Center is 20cm below eyes at 1.05m distance: atan2(0.20, 1.05) ~= 0.189 rad. - // Tilt of -0.20 rad angles the keyboard face normal directly at the user's eyes! + // Tilt upward toward eyes by -0.20 rad (~11.5 degrees) const float tilt = -0.20f; - vr::HmdMatrix34_t kbMat{}; - kbMat.m[0][0] = std::cos(yaw); - kbMat.m[0][1] = std::sin(yaw) * std::sin(tilt); - kbMat.m[0][2] = std::sin(yaw) * std::cos(tilt); - kbMat.m[0][3] = kbX; + vr::HmdMatrix34_t kbMatLocal{}; + kbMatLocal.m[0][0] = std::cos(yawLocal); + kbMatLocal.m[0][1] = std::sin(yawLocal) * std::sin(tilt); + kbMatLocal.m[0][2] = std::sin(yawLocal) * std::cos(tilt); + kbMatLocal.m[0][3] = kbLocalX; - kbMat.m[1][0] = 0.0f; - kbMat.m[1][1] = std::cos(tilt); - kbMat.m[1][2] = -std::sin(tilt); - kbMat.m[1][3] = kbY; + kbMatLocal.m[1][0] = 0.0f; + kbMatLocal.m[1][1] = std::cos(tilt); + kbMatLocal.m[1][2] = -std::sin(tilt); + kbMatLocal.m[1][3] = kbLocalY; - kbMat.m[2][0] = -std::sin(yaw); - kbMat.m[2][1] = std::cos(yaw) * std::sin(tilt); - kbMat.m[2][2] = std::cos(yaw) * std::cos(tilt); - kbMat.m[2][3] = kbZ; + kbMatLocal.m[2][0] = -std::sin(yawLocal); + kbMatLocal.m[2][1] = std::cos(yawLocal) * std::sin(tilt); + kbMatLocal.m[2][2] = std::cos(yawLocal) * std::cos(tilt); + kbMatLocal.m[2][3] = kbLocalZ; for (int r = 0; r < 3; ++r) { for (int c = 0; c < 4; ++c) { - kbMat_[r][c] = kbMat.m[r][c]; + kbMatLocal_[r][c] = kbMatLocal.m[r][c]; + } + } + + // ------------------------------------------------------------------------- + // 2. Calculate Stage transform (for SteamVR TrackingUniverseStanding overlay rendering) + // ------------------------------------------------------------------------- + float shx = hx, shy = hy + 1.65f, shz = hz; + float sfx = fx, sfz = fz; + if (headPosStage && headForwardStage) { + shx = headPosStage[0]; + shy = headPosStage[1]; + shz = headPosStage[2]; + sfx = headForwardStage[0]; + sfz = headForwardStage[2]; + } + float sflen = std::sqrt(sfx * sfx + sfz * sfz); + if (sflen < 1e-3f) { + sfx = 0.0f; + sfz = -1.0f; + } else { + sfx /= sflen; + sfz /= sflen; + } + const float yawStage = std::atan2(-sfx, -sfz); + + const float kbStageX = shx + sfx * dist; + const float kbStageY = shy - 0.22f; // In standing space (floor=0), shy ~1.65m, so kbStageY ~1.43m above floor! + const float kbStageZ = shz + sfz * dist; + + vr::HmdMatrix34_t kbMatStage{}; + kbMatStage.m[0][0] = std::cos(yawStage); + kbMatStage.m[0][1] = std::sin(yawStage) * std::sin(tilt); + kbMatStage.m[0][2] = std::sin(yawStage) * std::cos(tilt); + kbMatStage.m[0][3] = kbStageX; + + kbMatStage.m[1][0] = 0.0f; + kbMatStage.m[1][1] = std::cos(tilt); + kbMatStage.m[1][2] = -std::sin(tilt); + kbMatStage.m[1][3] = kbStageY; + + kbMatStage.m[2][0] = -std::sin(yawStage); + kbMatStage.m[2][1] = std::cos(yawStage) * std::sin(tilt); + kbMatStage.m[2][2] = std::cos(yawStage) * std::cos(tilt); + kbMatStage.m[2][3] = kbStageZ; + + for (int r = 0; r < 3; ++r) { + for (int c = 0; c < 4; ++c) { + kbMatStage_[r][c] = kbMatStage.m[r][c]; } } kbConfigured_ = false; // 1. Set keyboard transform in standing universe BEFORE ShowKeyboard - ov->SetKeyboardTransformAbsolute(vr::TrackingUniverseStanding, &kbMat); + ov->SetKeyboardTransformAbsolute(vr::TrackingUniverseStanding, &kbMatStage); - // 2. Invoke virtual keyboard + // 2. Invoke virtual keyboard with Modal | Minimal flags + // Minimal mode tells SteamVR GamepadUI not to show its internal text box + // and not to cache/accumulate dirty text drafts across sessions. vr::EVROverlayError err = ov->ShowKeyboard( mode, vr::k_EGamepadTextInputLineModeSingleLine, - vr::KeyboardFlag_Modal, + vr::KeyboardFlag_Modal | vr::KeyboardFlag_Minimal, description.c_str(), static_cast(maxChars), existingText.c_str(), - 0); + currentNonce_); if (err != vr::VROverlayError_None) { PIXELVR_LOG_WARN("VrKeyboard: ShowKeyboard failed with error %d", static_cast(err)); return false; } - // 3. Set keyboard transform AFTER ShowKeyboard as well - ov->SetKeyboardTransformAbsolute(vr::TrackingUniverseStanding, &kbMat); + // 3. Reinforce keyboard transform AFTER ShowKeyboard + ov->SetKeyboardTransformAbsolute(vr::TrackingUniverseStanding, &kbMatStage); // 4. Configure overlay laser and transform immediately if already present configureKeyboardOverlay(); isOpen_ = true; pollFramesSinceOpen_ = 0; - PIXELVR_LOG_INFO("VrKeyboard: Shown virtual keyboard for '%s' at pos (%.2f, %.2f, %.2f) yaw=%.2f", - description.c_str(), kbX, kbY, kbZ, yaw); + PIXELVR_LOG_INFO("VrKeyboard: Shown virtual keyboard for '%s' (nonce %llu, existing='%s') at stage pos (%.2f, %.2f, %.2f) local pos (%.2f, %.2f, %.2f)", + description.c_str(), static_cast(currentNonce_), + existingText.c_str(), kbStageX, kbStageY, kbStageZ, kbLocalX, kbLocalY, kbLocalZ); return true; } @@ -340,6 +385,7 @@ void VrKeyboard::hide() { kbConfigured_ = false; doneSubmitted_ = false; userTyped_ = false; + currentNonce_ = 0; currentText_.clear(); existingText_.clear(); PIXELVR_LOG_INFO("VrKeyboard: Hidden virtual keyboard"); @@ -355,47 +401,63 @@ void VrKeyboard::poll(std::function onDone, vr::VREvent_t event{}; while (sys->PollNextEvent(&event, sizeof(event))) { if (event.eventType == vr::VREvent_KeyboardCharInput) { - userTyped_ = true; - char keyBuf[1024] = {0}; - uint32_t len = ov->GetKeyboardText(keyBuf, sizeof(keyBuf)); - if (len > 0) { - currentText_ = std::string(keyBuf, len < sizeof(keyBuf) ? len : std::strlen(keyBuf)); - } else { - // Fallback: check event.data.keyboard.cNewInput - const char* inStr = event.data.keyboard.cNewInput; - if (inStr && inStr[0] != 0) { - const unsigned char firstByte = static_cast(inStr[0]); - if (firstByte == '\b' || firstByte == 0x7F || firstByte == 8) { - while (!currentText_.empty() && (static_cast(currentText_.back()) & 0xC0) == 0x80) { - currentText_.pop_back(); - } - if (!currentText_.empty()) { - currentText_.pop_back(); - } - } else if (firstByte >= 32) { + // Discard any events from old or stale keyboard sessions + if (event.data.keyboard.uUserValue != 0 && event.data.keyboard.uUserValue != currentNonce_) { + PIXELVR_LOG_INFO("VrKeyboard: Ignored stale CharInput (nonce %llu != current %llu)", + static_cast(event.data.keyboard.uUserValue), + static_cast(currentNonce_)); + continue; + } + + const char* inStr = event.data.keyboard.cNewInput; + if (inStr && inStr[0] != 0) { + userTyped_ = true; + const unsigned char firstByte = static_cast(inStr[0]); + if (firstByte == '' || firstByte == 0x7F || firstByte == 8) { + // Backspace: pop last UTF-8 character + while (!currentText_.empty() && (static_cast(currentText_.back()) & 0xC0) == 0x80) { + currentText_.pop_back(); + } + if (!currentText_.empty()) { + currentText_.pop_back(); + } + } else if (firstByte == '\n' || firstByte == '\r') { + isOpen_ = false; + kbConfigured_ = false; + doneSubmitted_ = true; + std::string text = currentText_; + ov->HideKeyboard(); + currentText_.clear(); + existingText_.clear(); + userTyped_ = false; + currentNonce_ = 0; + PIXELVR_LOG_INFO("VrKeyboard: User submitted text via Return key: '%s'", text.c_str()); + if (onDone) { + onDone(text); + } + return; + } else if (firstByte >= 32) { + if (currentText_.length() + std::strlen(inStr) <= static_cast(maxChars_)) { currentText_ += inStr; } } + PIXELVR_LOG_INFO("VrKeyboard: CharInput live text: '%s'", currentText_.c_str()); } - PIXELVR_LOG_INFO("VrKeyboard: CharInput live text: '%s'", currentText_.c_str()); } else if (event.eventType == vr::VREvent_KeyboardDone) { isOpen_ = false; kbConfigured_ = false; doneSubmitted_ = true; - char buf[1024] = {0}; - uint32_t len = ov->GetKeyboardText(buf, sizeof(buf)); std::string text = currentText_; - if (len > 0) { - text = std::string(buf, len < sizeof(buf) ? len : std::strlen(buf)); - } ov->HideKeyboard(); currentText_.clear(); existingText_.clear(); userTyped_ = false; + currentNonce_ = 0; PIXELVR_LOG_INFO("VrKeyboard: User submitted text via KeyboardDone: '%s'", text.c_str()); if (onDone) { onDone(text); } + return; } else if (event.eventType == vr::VREvent_KeyboardClosed || event.eventType == vr::VREvent_KeyboardClosed_Global) { isOpen_ = false; @@ -403,19 +465,19 @@ void VrKeyboard::poll(std::function onDone, ov->HideKeyboard(); PIXELVR_LOG_INFO("VrKeyboard: Keyboard closed (doneSubmitted=%d)", doneSubmitted_ ? 1 : 0); if (!doneSubmitted_) { - // Closed without KeyboardDone: treat as cancel and forget any input currentText_.clear(); existingText_.clear(); userTyped_ = false; + currentNonce_ = 0; PIXELVR_LOG_INFO("VrKeyboard: Discarded input on KeyboardClosed"); if (onCancel) { onCancel(); } } + return; } } - // Keep currentText_ synchronized with the live SteamVR keyboard buffer every frame if (isOpen_) { pollFramesSinceOpen_++; // Maintain and reinforce keyboard overlay configuration across the first 90 frames (~1 sec) @@ -423,16 +485,6 @@ void VrKeyboard::poll(std::function onDone, if (!kbConfigured_ && pollFramesSinceOpen_ < 90) { configureKeyboardOverlay(); } - - // Only synchronize with SteamVR live buffer if the user has actively typed in this session. - // This avoids contaminating a fresh/empty input field with stale residual buffer from a previous field. - if (userTyped_) { - char liveBuf[1024] = {0}; - uint32_t liveLen = ov->GetKeyboardText(liveBuf, sizeof(liveBuf)); - if (liveLen > 0) { - currentText_ = std::string(liveBuf, liveLen < sizeof(liveBuf) ? liveLen : std::strlen(liveBuf)); - } - } } } diff --git a/src/xr/vr_keyboard.hpp b/src/xr/vr_keyboard.hpp index 700fb7c..c26760b 100644 --- a/src/xr/vr_keyboard.hpp +++ b/src/xr/vr_keyboard.hpp @@ -23,8 +23,10 @@ public: const std::string& existingText, bool isPassword = false, int maxChars = 256, - const float* headPos = nullptr, - const float* headForward = nullptr); + const float* headPosLocal = nullptr, + const float* headForwardLocal = nullptr, + const float* headPosStage = nullptr, + const float* headForwardStage = nullptr); // Closes the virtual keyboard if open. void hide(); @@ -37,7 +39,7 @@ public: std::string currentText() const; const std::string& description() const { return description_; } bool isPassword() const { return isPassword_; } - const float* transform() const { return &kbMat_[0][0]; } + const float* transform() const { return &kbMatLocal_[0][0]; } private: VrKeyboard() = default; @@ -51,7 +53,10 @@ private: bool userTyped_ = false; bool kbConfigured_ = false; int pollFramesSinceOpen_ = 0; - float kbMat_[3][4]{}; + float kbMatLocal_[3][4]{}; + float kbMatStage_[3][4]{}; + uint64_t currentNonce_ = 0; + int maxChars_ = 256; std::string description_; std::string existingText_; std::string currentText_; diff --git a/src/xr/xr_context.cpp b/src/xr/xr_context.cpp index a594f14..fd01845 100644 --- a/src/xr/xr_context.cpp +++ b/src/xr/xr_context.cpp @@ -35,6 +35,10 @@ XrContext::~XrContext() { if (appSpace_ != XR_NULL_HANDLE) { xrDestroySpace(appSpace_); } + if (stageSpace_ != XR_NULL_HANDLE) { + xrDestroySpace(stageSpace_); + stageSpace_ = XR_NULL_HANDLE; + } if (actionSet_ != XR_NULL_HANDLE) { xrDestroyActionSet(actionSet_); } @@ -217,6 +221,18 @@ bool XrContext::createReferenceSpace() { createInfo.poseInReferenceSpace.orientation.w = 1.0f; PIXELVR_XR_CHECK(instance_, xrCreateReferenceSpace(session_, &createInfo, &appSpace_)); + + // Create STAGE reference space (floor level tracking bounds, matching SteamVR TrackingUniverseStanding) + XrReferenceSpaceCreateInfo stageInfo{XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; + stageInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; + stageInfo.poseInReferenceSpace.orientation.w = 1.0f; + XrResult stageRes = xrCreateReferenceSpace(session_, &stageInfo, &stageSpace_); + if (XR_SUCCEEDED(stageRes)) { + PIXELVR_LOG_INFO("XrContext: Successfully created STAGE reference space"); + } else { + PIXELVR_LOG_WARN("XrContext: STAGE reference space not available (%d), using calibrated standing fallback", static_cast(stageRes)); + stageSpace_ = XR_NULL_HANDLE; + } return true; } @@ -922,6 +938,31 @@ bool XrContext::renderFrame(const RenderViewFn& renderView) { input_.headForward[2] = -(1.0f - 2.0f * (q.x * q.x + q.y * q.y)); } + input_.stageValid = false; + if (stageSpace_ != XR_NULL_HANDLE && input_.headValid) { + XrSpaceLocation loc{XR_TYPE_SPACE_LOCATION}; + if (XR_SUCCEEDED(xrLocateSpace(appSpace_, stageSpace_, frameState.predictedDisplayTime, &loc))) { + constexpr XrSpaceLocationFlags need = + XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | + XR_SPACE_LOCATION_POSITION_VALID_BIT; + if ((loc.locationFlags & need) == need) { + input_.stageValid = true; + input_.appToStagePose = loc.pose; + transformToStage(input_.headPos, input_.headForward, + input_.headPosStage, input_.headForwardStage); + } + } + } + if (!input_.stageValid) { + // Fallback: Standing stage eye level is ~1.65m above the physical floor + input_.headPosStage[0] = input_.headPos[0]; + input_.headPosStage[1] = input_.headPos[1] + 1.65f; + input_.headPosStage[2] = input_.headPos[2]; + input_.headForwardStage[0] = input_.headForward[0]; + input_.headForwardStage[1] = input_.headForward[1]; + input_.headForwardStage[2] = input_.headForward[2]; + } + // Locate each controller's aim pose (for the pointer rays and models). for (int h = 0; h < 2; ++h) { input_.controllers[h].active = false; @@ -1028,4 +1069,31 @@ bool XrContext::renderFrame(const RenderViewFn& renderView) { return true; } +void XrContext::transformToStage(const float localPos[3], const float localFwd[3], + float stagePos[3], float stageFwd[3]) const { + const auto& p = input_.appToStagePose.position; + const auto& q = input_.appToStagePose.orientation; + + if (localPos && stagePos) { + const float vx = localPos[0], vy = localPos[1], vz = localPos[2]; + const float tw = -q.x * vx - q.y * vy - q.z * vz; + const float tx = q.w * vx + q.y * vz - q.z * vy; + const float ty = q.w * vy + q.z * vx - q.x * vz; + const float tz = q.w * vz + q.x * vy - q.y * vx; + stagePos[0] = p.x + (tx * q.w + tw * (-q.x) + ty * (-q.z) - tz * (-q.y)); + stagePos[1] = p.y + (ty * q.w + tw * (-q.y) + tz * (-q.x) - tx * (-q.z)); + stagePos[2] = p.z + (tz * q.w + tw * (-q.z) + tx * (-q.y) - ty * (-q.x)); + } + if (localFwd && stageFwd) { + const float fx = localFwd[0], fy = localFwd[1], fz = localFwd[2]; + const float ftw = -q.x * fx - q.y * fy - q.z * fz; + const float ftx = q.w * fx + q.y * fz - q.z * fy; + const float fty = q.w * fy + q.z * fx - q.x * fz; + const float ftz = q.w * fz + q.x * fy - q.y * fx; + stageFwd[0] = ftx * q.w + ftw * (-q.x) + fty * (-q.z) - ftz * (-q.y); + stageFwd[1] = fty * q.w + ftw * (-q.y) + ftz * (-q.x) - ftx * (-q.z); + stageFwd[2] = ftz * q.w + ftw * (-q.z) + ftx * (-q.y) - fty * (-q.x); + } +} + } // namespace pixelvr diff --git a/src/xr/xr_context.hpp b/src/xr/xr_context.hpp index a446797..d7e0515 100644 --- a/src/xr/xr_context.hpp +++ b/src/xr/xr_context.hpp @@ -34,6 +34,10 @@ struct XrInputState { bool headValid = false; float headPos[3] = {0.0f, 0.0f, 0.0f}; float headForward[3] = {0.0f, 0.0f, -1.0f}; + bool stageValid = false; + XrPosef appToStagePose{{0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 1.65f, 0.0f}}; + float headPosStage[3] = {0.0f, 1.65f, 0.0f}; + float headForwardStage[3] = {0.0f, 0.0f, -1.0f}; // Tracked controller aim poses (index 0 = left, 1 = right). The pointer ray // is the -Z axis of `orientation` from `position`, in the app reference space. @@ -122,7 +126,10 @@ public: XrInstance instance() const { return instance_; } XrSession session() const { return session_; } XrSpace appSpace() const { return appSpace_; } + XrSpace stageSpace() const { return stageSpace_; } XrTime displayTime() const { return lastDisplayTime_; } + void transformToStage(const float localPos[3], const float localFwd[3], + float stagePos[3], float stageFwd[3]) const; XrSystemId systemId() const { return systemId_; } bool renderModelEnabled() const { return renderModelEnabled_; } uint32_t viewCount() const { return static_cast(configViews_.size()); } @@ -145,6 +152,7 @@ private: XrSystemId systemId_ = XR_NULL_SYSTEM_ID; XrSession session_ = XR_NULL_HANDLE; XrSpace appSpace_ = XR_NULL_HANDLE; + XrSpace stageSpace_ = XR_NULL_HANDLE; XrTime lastDisplayTime_ = 0; XrSessionState sessionState_ = XR_SESSION_STATE_UNKNOWN;