6 Commits
9 changed files with 609 additions and 220 deletions

No files matched your search

+29 -8
View File
@@ -7,19 +7,40 @@
---
## Active backlog (2026-10-20 — Completed)
- [x] **Virtual Keyboard Tilt Inversion, Raycast Auto-Focus & UI Hide Refinements**
- **Symptom 1:** Keyboard spawned tilted with the top edge pitched forward (slanted away from user).
- **Resolution 1:** Corrected tilt angle to \-0.38f\ (-21.8 degrees). The top edge slants backward away from user; face normal tilts upward directly toward the user's eyes.
- **Symptom 2:** Laser beam shot 2.5m past the keyboard with no cursor dot on the keys; laser wasn't focused.
- **Resolution 2:** Added keyboard plane collision test in \VulkanRenderer::updatePointer()\ using \KeyboardModalData::transform\. Laser ray terminates directly on the keyboard surface with glowing cursor dot on hovered keys. Also ensured active controller laser turns on automatically when keyboard opens, and guarded background menus from eating keyboard trigger clicks.
- **Symptom 3:** Trigger pull while HUD dismissed didn't reactivate the UI unless hitting a hidden button; empty-space clicks re-dismissed the UI in the same frame.
- **Resolution 3:** Recorded \wasHudDismissed\ at frame start and guarded dock interactions with \!wasHudDismissed\. Trigger click cleanly un-hides the HUD without clicking hidden buttons or re-dismissing in empty space.
- **Symptom 4:** Analog grip squeeze was overly sensitive and accidentally reactivated the dismissed UI.
- **Resolution 4:** Ignored soft analog grip (\in.gripDown\ and \BtnGrip\); only digital full grip click stop (\in.gripFullPressDown\) un-hides the HUD.
## Active backlog (2026-10-19 — Completed)
- [x] **1. Fix Virtual Keyboard Laser Pointer Focus & Tracking**
- **Symptom:** Virtual keyboard opened, but laser pointer disappeared or wasn't focused on the keyboard; user had to squeeze/press the controller grip fully to activate it.
- **Symptom:** Virtual keyboard opened, but laser pointer disappeared or wasn't focused on the keyboard; user had to squeeze/press the controller grip fully to activate it. Also, the keyboard centered where the Steam menu was centered when the app launched instead of where the user centered their view.
- **Root Cause:**
- SteamVR's `vrcompositor` switches system panel interaction mode to Gamepad (mode 0, hiding `system.pointer`) on session focus loss unless an interactive overlay requiring laser mouse is currently visible (`bOverlaysPresentThatForceLaserMouseOn`).
- `inputOverlay_` was created in `VrKeyboard::init()` but was **never shown** (`ShowOverlay` was never called, had no texture, dimensions, or transform).
- SteamVR's virtual keyboard overlay `valve.steam.gamepadui.keyboard` is spawned asynchronously and lacks `MakeOverlaysInteractiveIfVisible` by default.
- SteamVR's virtual keyboard overlay key is `VRKeyboard_uid0` (from CEF `webhelper`), not `valve.steam.gamepadui.keyboard`. Previous lookups failed, so `MakeOverlaysInteractiveIfVisible` and `VROverlayInputMethod_Mouse` were never applied to the actual keyboard overlay.
- `VulkanRenderer::updatePointer()` was returning early (`if (keyboardModal_.visible) return;`), which prevented raycasts to the modal plane and left `pointerActive_` false with no cursor dot.
- `SetKeyboardTransformAbsolute` was never called with the user's current centered view orientation/position, so SteamVR defaulted to the launch standing origin.
- **Resolution:**
- Fully configured `inputOverlay_` in `VrKeyboard::show()`: set width (1.0m), HMD-relative transform (1.1m in front, tilted), 1x1 transparent raw pixel texture (`SetOverlayRaw`), and docked keyboard position (`SetKeyboardPositionForOverlay`).
- Called `ov->ShowOverlay(inputOverlay_)` before calling `ShowKeyboardForOverlay`, ensuring `vrcompositor`'s overlay scan sets `m_bOverlaysPresentThatForceLaserMouseOn = 1`.
- Added dynamic polling in `VrKeyboard::poll()` to detect and configure `MakeOverlaysInteractiveIfVisible` and `VROverlayInputMethod_Mouse` on `valve.steam.gamepadui.keyboard` as soon as it appears.
- Added proper overlay cleanup (`HideOverlay(inputOverlay_)`) in `hide()`, `KeyboardDone`, and `KeyboardClosed`.
- Implemented dynamic overlay discovery across handles 1..128 and known keys (`VRKeyboard*`, `valve.steam.gamepadui.keyboard`, `system.keyboard`).
- Configured `MakeOverlaysInteractiveIfVisible`, `VROverlayInputMethod_Mouse`, and `SendVRDiscreteScrollEvents` on the keyboard overlay.
- Calculated world matrix in `TrackingUniverseStanding` directly from the user's HMD pose (or OpenXR `headPos` and `headForward`): placed 1.05m in front of eyes, 0.38m below eye level, tilted 28° up toward the eyes.
- Applied transform via `SetKeyboardTransformAbsolute(vr::TrackingUniverseStanding, &kbMat)` and `SetOverlayTransformAbsolute`.
- Updated `VulkanRenderer::updatePointer()` to raycast against the modal plane when `keyboardModal_.visible` is active, ensuring sharp laser pointer focus and a visible cursor dot.
- Guarded `VrKeyboard::shutdown()` to prevent crash on exit.
- [x] **2. Controller Movement UI Dismissal Fix**
- **Symptom:** When the user clicked "Hide UI" or clicked outside controls, any hand movement immediately reactivated the UI.
- **Requirement:** Once dismissed, the UI must stay hidden regardless of controller movement/rotation, and only reactivate when the user explicitly presses a physical button on the controller.
- **Resolution:**
- In `App::run()`, when `hudDismissed_` is true, controller movement (`moveDist`) and rotation (`rotDiff`) are blocked from clearing `hudDismissed_` or setting `activity = true`.
- Only physical button presses (`in.togglePlay`, `in.browserToggle`, `in.recenter`, `in.buttonDown[...]`, `in.gripDown`, `in.gripFullPressDown`, `in.select` outside cooldown, or thumbstick movement) reactivate the HUD.
- [x] **2. Strict IPv4 Validation & Live Sanitization**
- **Symptom:** Host field accepted invalid characters, spaces, and did not strip leading zeros.
+75 -28
View File
@@ -313,6 +313,13 @@ void App::loadMedia(const std::string& path) {
}
hudDismissed_ = false;
lastControllerActivity_ = std::chrono::steady_clock::now();
VrKeyboard::instance().hide();
actionMenu_.visible = false;
smbModal_.active = false;
smbModal_.editingField = SmbModalState::Field::None;
browser_.setOpen(false);
downloadsWindow_ = false;
pendingDl_.clear();
// Refine the stereo layout from the frame aspect ratio when the filename did
// not state it: once the projection is a VR type the aspect is unambiguous
@@ -665,6 +672,8 @@ void App::saveSmbModal() {
SmbCredentialsManager::instance().addOrUpdateShare(s);
smbModal_.active = false;
smbModal_.editingField = SmbModalState::Field::None;
VrKeyboard::instance().hide();
actionMenu_.visible = false;
browser_.openNetwork();
#endif
@@ -676,6 +685,8 @@ void App::deleteSmbModal() {
SmbCredentialsManager::instance().deleteShare(smbModal_.origHost, smbModal_.origShare);
}
smbModal_.active = false;
smbModal_.editingField = SmbModalState::Field::None;
VrKeyboard::instance().hide();
actionMenu_.visible = false;
browser_.openNetwork();
#endif
@@ -840,7 +851,8 @@ int App::run() {
renderer_.setKeyboardModal(kbOpen,
VrKeyboard::instance().description(),
kbDisplayText,
VrKeyboard::instance().isPassword());
VrKeyboard::instance().isPassword(),
kbOpen ? VrKeyboard::instance().transform() : nullptr);
if (kbOpen) {
in.select = false;
in.selectHeld = false;
@@ -1034,6 +1046,7 @@ int App::run() {
// Controller activity: moving a controller past a small threshold (or
// any button/stick input) keeps the laser, models and controls shown;
// after a few seconds of stillness they fade away.
const bool wasHudDismissed = hudDismissed_;
const auto timeSinceDismiss = nowT - hudDismissTime_;
const bool inDismissCooldown = hudDismissed_ && (timeSinceDismiss < std::chrono::milliseconds(500));
bool activity = false;
@@ -1060,13 +1073,10 @@ int App::run() {
const float rotDiff = std::fabs(od);
if (hudDismissed_) {
// Requires intentional hand movement after dismissal cooldown to re-engage HUD
if (!inDismissCooldown && (moveDist > 0.12f || rotDiff < 0.950f)) {
hudDismissed_ = false;
activity = true;
for (int k = 0; k < 3; ++k) activityAnchorPos_[h][k] = c.position[k];
for (int k = 0; k < 4; ++k) activityAnchorOrient_[h][k] = c.orientation[k];
}
// Controller movement MUST NEVER un-hide the HUD when dismissed.
// Keep activity anchors updated so they don't accumulate delta.
for (int k = 0; k < 3; ++k) activityAnchorPos_[h][k] = c.position[k];
for (int k = 0; k < 4; ++k) activityAnchorOrient_[h][k] = c.orientation[k];
} else if (moveDist > 0.06f || rotDiff < 0.985f) {
// Normal threshold: filters micro-tremors from resting hands
activity = true;
@@ -1078,7 +1088,23 @@ int App::run() {
std::fabs(in.thumbstickY) > 0.25f ||
std::fabs(in.thumbstickRX) > 0.25f ||
std::fabs(in.thumbstickRY) > 0.25f);
if (in.togglePlay || in.browserToggle || in.recenter || stickMoved) {
// Only digital full grip press (gripFullPressDown) or physical buttons wake the HUD.
// Soft analog squeeze (gripDown / BtnGrip) is ignored to prevent accidental wake-up from resting hands.
bool anyButtonPressed = (in.togglePlay || in.browserToggle || in.recenter ||
in.gripFullPressDown[0] || in.gripFullPressDown[1] ||
stickMoved);
for (int b = 0; b < XrInputState::BtnCount; ++b) {
if (b == XrInputState::BtnGrip) continue;
if (in.buttonDown[b]) {
anyButtonPressed = true;
break;
}
}
if (!inDismissCooldown && in.select) {
anyButtonPressed = true;
}
if (anyButtonPressed) {
activity = true;
if (!inDismissCooldown) {
hudDismissed_ = false;
@@ -1100,12 +1126,18 @@ int App::run() {
if (in.browserToggle) {
if (actionMenu_.visible) {
actionMenu_.visible = false;
smbModal_.active = false;
smbModal_.editingField = SmbModalState::Field::None;
VrKeyboard::instance().hide();
} else if (downloadsWindow_) {
downloadsWindow_ = false;
} else if (settings_.isOpen()) {
settings_.close();
} else if (browser_.isOpen()) {
browser_.setOpen(false);
smbModal_.active = false;
smbModal_.editingField = SmbModalState::Field::None;
VrKeyboard::instance().hide();
} else if (mediaOpen_ && renderer_.controlBarVisible()) {
// HUD is visible during playback: B/Y dismisses the HUD!
dismissHud(in, nowT);
@@ -1529,11 +1561,11 @@ int App::run() {
}
}
const bool uiPanelActive = browserOpen || settingsOpen ||
downloadsWindow_ || !pendingDl_.empty() || actionMenu_.visible;
downloadsWindow_ || !pendingDl_.empty() || actionMenu_.visible || smbModal_.active;
// The Transport HUD is only visible during playback when the
// Unified Spatial Hub and modals are closed.
const bool barBlocked = browserOpen || settingsOpen || downloadsWindow_ ||
!pendingDl_.empty() || actionMenu_.visible;
!pendingDl_.empty() || actionMenu_.visible || smbModal_.active;
// Controls (buttons + timeline) show only while a controller is
// actively moving; they fade after a few seconds of stillness.
renderer_.setControlBar(!barBlocked && controllersEngaged && !hudDismissed_ &&
@@ -1541,8 +1573,15 @@ int App::run() {
in.controllers[1].active));
// Keep laser active when UI panels, activity, or virtual keyboard are active!
const bool kbActive = VrKeyboard::instance().isOpen();
if (kbActive && !renderer_.laserOn(0) && !renderer_.laserOn(1)) {
renderer_.setLaserOn(1, true);
if (kbActive) {
if (!renderer_.laserOn(0) && !renderer_.laserOn(1)) {
if (in.controllers[1].active) renderer_.setLaserOn(1, true);
else if (in.controllers[0].active) renderer_.setLaserOn(0, true);
} else if (renderer_.laserOn(1) && !in.controllers[1].active && in.controllers[0].active) {
renderer_.setLaserOn(0, true);
} else if (renderer_.laserOn(0) && !in.controllers[0].active && in.controllers[1].active) {
renderer_.setLaserOn(1, true);
}
}
const bool showLaser = (uiPanelActive || (controllersEngaged && !hudDismissed_) || kbActive);
// Build each controller's render state: corrected model pose,
@@ -1881,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);
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);
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);
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);
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);
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);
openKeyboardForField(SmbModalState::Field::Label, "SMB Nickname", smbModal_.label, false);
} else if (chosen == 7) {
// Save SMB Share
saveSmbModal();
@@ -1910,6 +1952,8 @@ int App::run() {
// Cancel
actionMenu_.visible = false;
smbModal_.active = false;
smbModal_.editingField = SmbModalState::Field::None;
VrKeyboard::instance().hide();
} else {
// Delete Share
deleteSmbModal();
@@ -1918,6 +1962,8 @@ int App::run() {
// Cancel
actionMenu_.visible = false;
smbModal_.active = false;
smbModal_.editingField = SmbModalState::Field::None;
VrKeyboard::instance().hide();
}
}
interacted = true;
@@ -2089,7 +2135,7 @@ int App::run() {
// Cinema Transport Dock is only active when the Unified Spatial Hub is closed.
const bool barOk = !browserOpen && !settingsOpen && !downloadsWindow_ &&
pendingDl_.empty() && !actionMenu_.visible && !smbModal_.active;
if (barOk && renderer_.controlBarVisible()) {
if (barOk && renderer_.controlBarVisible() && !wasHudDismissed) {
// Point-and-click the media control bar with the trigger.
const int btn = renderer_.pointerButton();
// The trigger clicks the bar; A does too (A is the reliable
@@ -2187,7 +2233,7 @@ int App::run() {
interacted = true;
}
// Click outside the control dock in empty space immediately hides the HUD
if (in.select && btn < 0 && !renderer_.pointerOnTimeline() && !renderer_.pointerOnTimeToggle()) {
if (in.select && !renderer_.pointerActive()) {
dismissHud(in, nowT);
interacted = true;
}
@@ -2254,7 +2300,7 @@ int App::run() {
// hardware decoder (a reopen each) and stalls it; one seek per
// gesture stays responsive.
bool scrubInput = false;
if (renderer_.pointerOnTimeline() && in.selectHeld &&
if (!wasHudDismissed && renderer_.pointerOnTimeline() && in.selectHeld &&
media_.duration() > 0.0) {
scrubTarget_ = renderer_.pointerScrub() * media_.duration();
scrubInput = true;
@@ -2385,6 +2431,7 @@ int App::run() {
if (renderer_.device() != VK_NULL_HANDLE) {
vkDeviceWaitIdle(renderer_.device());
}
VrKeyboard::instance().shutdown();
renderModels_.shutdown(); // destroy XR model handles before the session
PIXELVR_LOG_INFO("Shutting down");
return 0;
+55 -6
View File
@@ -1249,12 +1249,13 @@ void VulkanRenderer::updatePointer() {
actionMenuKeyHovered_ = -1;
const bool actionMenuOpen = actionMenu_.visible;
const bool kbOpen = keyboardModal_.visible;
const bool hubOpen = (browserVisible_ || settingsVisible_ ||
(downloadsVisible_ && !downloadsModal_)) && !actionMenuOpen;
const bool modalOpen = (downloadsVisible_ && downloadsModal_) && !actionMenuOpen;
const bool controlBar = controlBarVisible_ && !hubOpen && !modalOpen && !actionMenuOpen;
if (keyboardModal_.visible || (!hubOpen && !modalOpen && !controlBar && !actionMenuOpen)) {
return; // Virtual keyboard active or no UI surface to point at.
(downloadsVisible_ && !downloadsModal_)) && !actionMenuOpen && !kbOpen;
const bool modalOpen = (downloadsVisible_ && downloadsModal_) && !actionMenuOpen && !kbOpen;
const bool controlBar = controlBarVisible_ && !hubOpen && !modalOpen && !actionMenuOpen && !kbOpen;
if (!kbOpen && !hubOpen && !modalOpen && !controlBar && !actionMenuOpen) {
return; // No UI surface to point at.
}
using namespace math;
@@ -1309,7 +1310,55 @@ void VulkanRenderer::updatePointer() {
const float hyy = oL.y + t * dL.y;
const Vec3 world = origin + dir * t;
// 0. File Action Submenu Modal takes highest priority
// 0. Virtual Keyboard takes top focus priority
if (kbOpen) {
// First priority: Check if pointing directly at the virtual keyboard overlay keys
if (keyboardModal_.hasTransform) {
const auto& m = keyboardModal_.transform;
// Column 0: Local X axis (right)
const Vec3 kbRight{m[0][0], m[1][0], m[2][0]};
// Column 1: Local Y axis (up)
const Vec3 kbUp{m[0][1], m[1][1], m[2][1]};
// Column 2: Local Z axis (face normal pointing toward viewer)
const Vec3 kbNormal{m[0][2], m[1][2], m[2][2]};
// Column 3: Translation (center)
const Vec3 kbCenter{m[0][3], m[1][3], m[2][3]};
const float denom = dot(dir, kbNormal);
if (denom < -1e-4f) { // Ray points into front of keyboard
const float tKb = dot(kbCenter - origin, kbNormal) / denom;
if (tKb > 0.05f && tKb < 3.5f) {
const Vec3 hitP = origin + dir * tKb;
const Vec3 rel = hitP - kbCenter;
const float localX = dot(rel, kbRight);
const float localY = dot(rel, kbUp);
// Overlay width is 1.05m (halfW = 0.525m). Height is ~0.40m (halfH = 0.20m).
// Allow generous interactive margin (0.58m x 0.26m)
if (std::fabs(localX) <= 0.58f && std::fabs(localY) <= 0.26f) {
pointerActive_ = true;
pointerHand_ = h;
pointerHit_[0] = hitP.x;
pointerHit_[1] = hitP.y;
pointerHit_[2] = hitP.z;
break;
}
}
}
}
// Second priority: Check if pointing at the floating modal text box directly above the keyboard
// Modal box center is cy = 0.16f, half-width 0.23f, half-height 0.13f
if (std::fabs(hx) <= 0.25f && std::fabs(hyy - 0.16f) <= 0.14f) {
pointerActive_ = true;
pointerHand_ = h;
pointerHit_[0] = world.x;
pointerHit_[1] = world.y;
pointerHit_[2] = world.z;
break;
}
continue;
}
// 1. File Action Submenu Modal takes next priority
if (actionMenuOpen) {
if (hx >= -kPanelW && hx <= kPanelW && hyy >= -kPanelTop && hyy <= kPanelTop) {
pointerActive_ = true;
+13 -1
View File
@@ -4,6 +4,7 @@
#include "xr/openxr_headers.hpp"
#include <cstdint>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
@@ -95,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.
@@ -189,13 +192,22 @@ public:
std::string prompt;
std::string text;
bool isPassword = false;
bool hasTransform = false;
float transform[3][4]{};
};
void setKeyboardModal(bool visible, const std::string& prompt,
const std::string& text, bool isPassword = false) {
const std::string& text, bool isPassword = false,
const float* transform = nullptr) {
keyboardModal_.visible = visible;
keyboardModal_.prompt = prompt;
keyboardModal_.text = text;
keyboardModal_.isPassword = isPassword;
if (transform && visible) {
std::memcpy(keyboardModal_.transform, transform, sizeof(keyboardModal_.transform));
keyboardModal_.hasTransform = true;
} else {
keyboardModal_.hasTransform = false;
}
}
const KeyboardModalData& keyboardModal() const { return keyboardModal_; }
+4 -3
View File
@@ -586,7 +586,8 @@ void Browser::scanBookmarksInto(ScanResult& out) {
if (bm.path.empty()) continue;
const bool isSmb = (bm.path.rfind("smb://", 0) == 0);
std::error_code ec;
const bool isDir = isSmb || fs::is_directory(bm.path, ec);
const bool isVid = is_video(bm.path);
const bool isDir = isSmb ? !isVid : fs::is_directory(bm.path, ec);
std::string label = bm.label;
if (label.empty()) {
if (isSmb) {
@@ -598,9 +599,9 @@ void Browser::scanBookmarksInto(ScanResult& out) {
if (label.empty()) label = bm.path;
}
}
std::string detail = isSmb ? "SMB Share" : (isDir ? "Folder" : "Video File");
std::string detail = isSmb ? (isVid ? "SMB Video" : "SMB Share") : (isDir ? "Folder" : "Video File");
std::string previewPath = isDir ? "" : bm.path;
out.entries.push_back({label, bm.path, isDir, !isDir && is_video(bm.path), false, isSmb, detail, 0, previewPath});
out.entries.push_back({label, bm.path, isDir, isVid, false, false, detail, 0, previewPath});
}
}
+343 -172
View File
@@ -1,9 +1,19 @@
#include "vr_keyboard.hpp"
#include "util/logging.hpp"
#include <cstring>
#include <cmath>
#include <chrono>
#if defined(__linux__) || defined(__APPLE__)
#include <dlfcn.h>
#if defined(__linux__)
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/stat.h>
#include <algorithm>
#endif
#else
#include <windows.h>
#endif
@@ -22,6 +32,48 @@ 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;
#if defined(__linux__)
static void clearOrSetShmKeyboardBuffer(const std::string& text) {
// SteamVR stores the active keyboard buffer across all GamepadUI CEF instances
// in a shared memory segment of size 0x40cc0 (265408 bytes) named /dev/shm/u<uid>-Shm_*
// at offset 0x3fe60.
DIR* dir = opendir("/dev/shm");
if (!dir) return;
struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
if (std::strstr(entry->d_name, "Shm_") != nullptr) {
std::string path = std::string("/dev/shm/") + entry->d_name;
struct stat st{};
if (stat(path.c_str(), &st) == 0 && st.st_size == 0x40cc0) {
int fd = open(path.c_str(), O_RDWR);
if (fd >= 0) {
void* ptr = mmap(nullptr, 0x40cc0, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
if (ptr != MAP_FAILED) {
char* kbBuf = reinterpret_cast<char*>(ptr) + 0x3fe60;
std::memset(kbBuf, 0, 1024);
if (!text.empty()) {
std::size_t copyLen = std::min(text.length(), static_cast<std::size_t>(1023));
std::memcpy(kbBuf, text.c_str(), copyLen);
}
msync(ptr, 0x40cc0, MS_SYNC);
munmap(ptr, 0x40cc0);
PIXELVR_LOG_INFO("VrKeyboard: Updated shm %s at 0x3fe60 to '%s'",
entry->d_name, text.c_str());
}
}
}
}
}
closedir(dir);
}
#else
static void clearOrSetShmKeyboardBuffer(const std::string&) {}
#endif
} // namespace
VrKeyboard& VrKeyboard::instance() {
@@ -106,19 +158,8 @@ bool VrKeyboard::init() {
vrOverlay_ = ov;
available_ = true;
// Create a persistent input overlay so ShowKeyboardForOverlay can link
// tracked controller pointer interactions directly to SteamVR's virtual keyboard.
vr::VROverlayHandle_t hOverlay = vr::k_ulOverlayHandleInvalid;
vr::EVROverlayError ovErr = ov->CreateOverlay("pixelvr.input_overlay", "PixelVR Input", &hOverlay);
if (ovErr == vr::VROverlayError_None && hOverlay != vr::k_ulOverlayHandleInvalid) {
inputOverlay_ = hOverlay;
ov->SetOverlayInputMethod(hOverlay, vr::VROverlayInputMethod_Mouse);
ov->SetOverlayFlag(hOverlay, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
PIXELVR_LOG_INFO("VrKeyboard: Created input overlay %llu with Mouse input method and MakeOverlaysInteractiveIfVisible",
static_cast<unsigned long long>(inputOverlay_));
} else {
PIXELVR_LOG_WARN("VrKeyboard: Could not create input overlay (err=%d)", static_cast<int>(ovErr));
}
// Clear any stale shared memory keyboard buffer left by previous sessions
clearOrSetShmKeyboardBuffer("");
PIXELVR_LOG_INFO("VrKeyboard: Successfully initialized SteamVR overlay interface (sys=%p, ov=%p)",
reinterpret_cast<void*>(sys), reinterpret_cast<void*>(ov));
@@ -126,16 +167,15 @@ bool VrKeyboard::init() {
}
void VrKeyboard::shutdown() {
if (!available_) return;
available_ = false;
if (isOpen_) {
hide();
}
if (inputOverlay_ != 0 && vrOverlay_) {
auto* ov = reinterpret_cast<vr::IVROverlay*>(vrOverlay_);
ov->DestroyOverlay(inputOverlay_);
inputOverlay_ = 0;
}
if (g_VR_ShutdownInternal) {
g_VR_ShutdownInternal();
auto fn = g_VR_ShutdownInternal;
g_VR_ShutdownInternal = nullptr;
fn();
}
vrSystem_ = nullptr;
vrOverlay_ = nullptr;
@@ -148,108 +188,245 @@ void VrKeyboard::shutdown() {
libHandle_ = nullptr;
}
g_VR_InitInternal = nullptr;
g_VR_ShutdownInternal = nullptr;
g_VR_GetGenericInterface = nullptr;
available_ = false;
isOpen_ = false;
doneSubmitted_ = false;
kbConfigured_ = false;
currentNonce_ = 0;
}
bool VrKeyboard::show(const std::string& description,
const std::string& existingText,
bool isPassword,
int maxChars) {
if (!available_ && !init()) {
return false;
}
bool VrKeyboard::configureKeyboardOverlay() {
auto* ov = reinterpret_cast<vr::IVROverlay*>(vrOverlay_);
if (!ov) return false;
description_ = description;
existingText_ = existingText;
currentText_ = existingText;
isPassword_ = isPassword;
doneSubmitted_ = false;
const vr::EGamepadTextInputMode mode = isPassword
? vr::k_EGamepadTextInputModePassword
: vr::k_EGamepadTextInputModeNormal;
vr::EVROverlayError err = vr::VROverlayError_None;
if (inputOverlay_ != 0) {
ov->SetOverlayInputMethod(inputOverlay_, vr::VROverlayInputMethod_Mouse);
ov->SetOverlayFlag(inputOverlay_, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
ov->SetOverlayWidthInMeters(inputOverlay_, 1.0f);
// Position overlay 1.1m in front of HMD, tilted slightly down/up to match comfortable view
vr::HmdMatrix34_t mat = {
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, -0.2f,
0.0f, 0.0f, 1.0f, -1.1f
};
ov->SetOverlayTransformTrackedDeviceRelative(inputOverlay_, vr::k_unTrackedDeviceIndex_Hmd, &mat);
// Transparent 1x1 pixel texture so overlay is valid and active without rendering visual artifacts
static const uint32_t s_transparentPixel = 0x00000000;
ov->SetOverlayRaw(inputOverlay_, const_cast<uint32_t*>(&s_transparentPixel), 1, 1, 4);
// Tell keyboard to avoid overlay upper area and appear cleanly docked
vr::HmdRect2_t avoidRect = {
{ 0.0f, 0.0f },
{ 1.0f, 0.2f }
};
ov->SetKeyboardPositionForOverlay(inputOverlay_, avoidRect);
// Show overlay BEFORE opening keyboard so vrcompositor recognizes active interactive overlay
ov->ShowOverlay(inputOverlay_);
err = ov->ShowKeyboardForOverlay(
inputOverlay_,
mode,
vr::k_EGamepadTextInputLineModeSingleLine,
vr::KeyboardFlag_Modal,
description.c_str(),
static_cast<uint32_t>(maxChars),
existingText.c_str(),
0);
} else {
err = ov->ShowKeyboard(
mode,
vr::k_EGamepadTextInputLineModeSingleLine,
vr::KeyboardFlag_Modal,
description.c_str(),
static_cast<uint32_t>(maxChars),
existingText.c_str(),
0);
}
if (err != vr::VROverlayError_None) {
PIXELVR_LOG_WARN("VrKeyboard: ShowKeyboard failed with error %d", static_cast<int>(err));
if (inputOverlay_ != 0) {
ov->HideOverlay(inputOverlay_);
bool foundAny = false;
vr::HmdMatrix34_t mat{};
for (int r = 0; r < 3; ++r) {
for (int c = 0; c < 4; ++c) {
mat.m[r][c] = kbMatStage_[r][c];
}
return false;
}
kbConfigured_ = false;
// Attempt early configuration of keyboard overlays if already created
auto configureHandle = [&](vr::VROverlayHandle_t h, const char* name) {
ov->SetOverlayTransformAbsolute(h, vr::TrackingUniverseStanding, &mat);
ov->SetOverlayWidthInMeters(h, 1.05f);
ov->SetOverlayInputMethod(h, vr::VROverlayInputMethod_Mouse);
ov->SetOverlayFlag(h, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
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 at standing pos (%.2f, %.2f, %.2f)",
static_cast<unsigned long long>(h), name ? name : "unknown",
mat.m[0][3], mat.m[1][3], mat.m[2][3]);
};
// 1. Check known specific overlay keys
const char* kbOverlayNames[] = {
"VRKeyboard_uid0",
"VRKeyboard_uid1",
"VRKeyboard",
"valve.steam.gamepadui.keyboard",
"system.keyboard"
};
for (const char* name : kbOverlayNames) {
vr::VROverlayHandle_t hKb = vr::k_ulOverlayHandleInvalid;
if (ov->FindOverlay(name, &hKb) == vr::VROverlayError_None && hKb != vr::k_ulOverlayHandleInvalid) {
ov->SetOverlayFlag(hKb, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
ov->SetOverlayInputMethod(hKb, vr::VROverlayInputMethod_Mouse);
kbConfigured_ = true;
PIXELVR_LOG_INFO("VrKeyboard: Configured interactive laser mouse for overlay '%s'", name);
configureHandle(hKb, name);
}
}
// 2. Scan active overlay handles (1..128) to catch any dynamic uid variations (e.g. VRKeyboard_uid2726460308)
for (uint64_t h = 1; h <= 128; ++h) {
char keyBuf[256] = {0};
vr::EVROverlayError kErr = vr::VROverlayError_None;
uint32_t len = ov->GetOverlayKey(h, keyBuf, sizeof(keyBuf), &kErr);
if (kErr == vr::VROverlayError_None && len > 0) {
if (std::strstr(keyBuf, "keyboard") != nullptr ||
std::strstr(keyBuf, "Keyboard") != nullptr ||
std::strstr(keyBuf, "VRKeyboard") != nullptr) {
configureHandle(h, keyBuf);
}
}
}
if (foundAny) {
kbConfigured_ = true;
}
return foundAny;
}
bool VrKeyboard::show(const std::string& description,
const std::string& existingText,
bool isPassword,
int maxChars,
const float* headPosLocal,
const float* headForwardLocal,
const float* headPosStage,
const float* headForwardStage) {
if (!available_ && !init()) {
return false;
}
auto* ov = reinterpret_cast<vr::IVROverlay*>(vrOverlay_);
if (!ov) return false;
if (isOpen_) {
hide();
}
// Explicitly hide any existing keyboard session in SteamVR to clear lingering state
ov->HideKeyboard();
// Reset shared memory keyboard buffer to existingText
clearOrSetShmKeyboardBuffer(existingText);
currentNonce_ = ++sKeyboardNonce;
description_ = description;
existingText_ = existingText;
currentText_ = existingText;
isPassword_ = isPassword;
maxChars_ = maxChars;
doneSubmitted_ = false;
userTyped_ = false;
const vr::EGamepadTextInputMode mode = isPassword
? vr::k_EGamepadTextInputModePassword
: vr::k_EGamepadTextInputModeNormal;
// -------------------------------------------------------------------------
// 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 (headPosLocal && headForwardLocal) {
hx = headPosLocal[0];
hy = headPosLocal[1];
hz = headPosLocal[2];
fx = headForwardLocal[0];
fz = headForwardLocal[2];
}
float flen = std::sqrt(fx * fx + fz * fz);
if (flen < 1e-3f) {
fx = 0.0f;
fz = -1.0f;
} else {
fx /= flen;
fz /= flen;
}
const float yawLocal = std::atan2(-fx, -fz);
// 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 -0.20 rad (~11.5 degrees)
const float tilt = -0.20f;
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;
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;
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) {
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, &kbMatStage);
// 2. Invoke virtual keyboard with Modal flag
vr::EVROverlayError err = ov->ShowKeyboard(
mode,
vr::k_EGamepadTextInputLineModeSingleLine,
vr::KeyboardFlag_Modal | vr::KeyboardFlag_Minimal,
description.c_str(),
static_cast<uint32_t>(maxChars),
existingText.c_str(),
currentNonce_);
if (err != vr::VROverlayError_None) {
PIXELVR_LOG_WARN("VrKeyboard: ShowKeyboard failed with error %d", static_cast<int>(err));
return false;
}
// Reinforce existingText in shared memory so GamepadUI CEF reads it cleanly
clearOrSetShmKeyboardBuffer(existingText);
// 3. Reinforce keyboard transform AFTER ShowKeyboard
ov->SetKeyboardTransformAbsolute(vr::TrackingUniverseStanding, &kbMatStage);
// 4. Configure overlay laser and transform immediately if already present
configureKeyboardOverlay();
isOpen_ = true;
PIXELVR_LOG_INFO("VrKeyboard: Shown virtual keyboard for '%s' (initial text: '%s')",
description.c_str(), existingText.c_str());
pollFramesSinceOpen_ = 0;
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<unsigned long long>(currentNonce_),
existingText.c_str(), kbStageX, kbStageY, kbStageZ, kbLocalX, kbLocalY, kbLocalZ);
return true;
}
@@ -258,13 +435,15 @@ void VrKeyboard::hide() {
auto* ov = reinterpret_cast<vr::IVROverlay*>(vrOverlay_);
if (ov) {
ov->HideKeyboard();
if (inputOverlay_ != 0) {
ov->HideOverlay(inputOverlay_);
}
}
clearOrSetShmKeyboardBuffer("");
isOpen_ = false;
kbConfigured_ = false;
doneSubmitted_ = false;
userTyped_ = false;
currentNonce_ = 0;
currentText_.clear();
existingText_.clear();
PIXELVR_LOG_INFO("VrKeyboard: Hidden virtual keyboard");
}
@@ -278,101 +457,93 @@ void VrKeyboard::poll(std::function<void(const std::string&)> onDone,
vr::VREvent_t event{};
while (sys->PollNextEvent(&event, sizeof(event))) {
if (event.eventType == vr::VREvent_KeyboardCharInput) {
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<unsigned char>(inStr[0]);
if (firstByte == '\b' || firstByte == 0x7F || firstByte == 8) {
while (!currentText_.empty() && (static_cast<unsigned char>(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<unsigned long long>(event.data.keyboard.uUserValue),
static_cast<unsigned long long>(currentNonce_));
continue;
}
const char* inStr = event.data.keyboard.cNewInput;
if (inStr && inStr[0] != 0) {
userTyped_ = true;
const unsigned char firstByte = static_cast<unsigned char>(inStr[0]);
if (firstByte == '\b' || firstByte == 0x7F || firstByte == 8) {
// Backspace: pop last UTF-8 character
while (!currentText_.empty() && (static_cast<unsigned char>(currentText_.back()) & 0xC0) == 0x80) {
currentText_.pop_back();
}
if (!currentText_.empty()) {
currentText_.pop_back();
}
PIXELVR_LOG_INFO("VrKeyboard: Backspace pressed, text now: '%s'", currentText_.c_str());
} else if (firstByte == '\n' || firstByte == '\r') {
isOpen_ = false;
kbConfigured_ = false;
doneSubmitted_ = true;
std::string text = currentText_;
ov->HideKeyboard();
clearOrSetShmKeyboardBuffer("");
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<size_t>(maxChars_)) {
currentText_ += inStr;
}
PIXELVR_LOG_INFO("VrKeyboard: Input '%s', text now: '%s'", inStr, 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;
if (inputOverlay_ != 0 && ov) {
ov->HideOverlay(inputOverlay_);
}
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();
clearOrSetShmKeyboardBuffer("");
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;
kbConfigured_ = false;
if (inputOverlay_ != 0 && ov) {
ov->HideOverlay(inputOverlay_);
}
ov->HideKeyboard();
clearOrSetShmKeyboardBuffer("");
PIXELVR_LOG_INFO("VrKeyboard: Keyboard closed (doneSubmitted=%d)", doneSubmitted_ ? 1 : 0);
if (!doneSubmitted_) {
// On Steam Frame / SteamOS, pressing the checkmark or closing the keyboard issues KeyboardClosed
// without KeyboardDone. Read the buffer and treat non-empty / modified input as submit!
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));
}
if (!text.empty() || text != existingText_) {
PIXELVR_LOG_INFO("VrKeyboard: Treating KeyboardClosed as submit with text: '%s'", text.c_str());
doneSubmitted_ = true;
if (onDone) {
onDone(text);
}
} else {
PIXELVR_LOG_INFO("VrKeyboard: Keyboard cancelled without text change");
if (onCancel) {
onCancel();
}
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_) {
// Dynamically ensure SteamVR keyboard overlay has laser mouse flags as soon as spawned
if (!kbConfigured_) {
const char* kbOverlayNames[] = {
"valve.steam.gamepadui.keyboard",
"system.keyboard"
};
for (const char* name : kbOverlayNames) {
vr::VROverlayHandle_t hKb = vr::k_ulOverlayHandleInvalid;
if (ov->FindOverlay(name, &hKb) == vr::VROverlayError_None && hKb != vr::k_ulOverlayHandleInvalid) {
ov->SetOverlayFlag(hKb, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
ov->SetOverlayInputMethod(hKb, vr::VROverlayInputMethod_Mouse);
kbConfigured_ = true;
PIXELVR_LOG_INFO("VrKeyboard: Configured interactive laser mouse for overlay '%s'", name);
}
}
}
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));
pollFramesSinceOpen_++;
// Maintain and reinforce keyboard overlay configuration across the first 90 frames (~1 sec)
// until successfully configured
if (!kbConfigured_ && pollFramesSinceOpen_ < 90) {
configureKeyboardOverlay();
}
}
}
+14 -2
View File
@@ -22,7 +22,11 @@ public:
bool show(const std::string& description,
const std::string& existingText,
bool isPassword = false,
int maxChars = 256);
int maxChars = 256,
const float* headPosLocal = nullptr,
const float* headForwardLocal = nullptr,
const float* headPosStage = nullptr,
const float* headForwardStage = nullptr);
// Closes the virtual keyboard if open.
void hide();
@@ -35,15 +39,24 @@ public:
std::string currentText() const;
const std::string& description() const { return description_; }
bool isPassword() const { return isPassword_; }
const float* transform() const { return &kbMatLocal_[0][0]; }
private:
VrKeyboard() = default;
~VrKeyboard();
bool configureKeyboardOverlay();
bool available_ = false;
bool isOpen_ = false;
bool doneSubmitted_ = false;
bool userTyped_ = false;
bool kbConfigured_ = false;
int pollFramesSinceOpen_ = 0;
float kbMatLocal_[3][4]{};
float kbMatStage_[3][4]{};
uint64_t currentNonce_ = 0;
int maxChars_ = 256;
std::string description_;
std::string existingText_;
std::string currentText_;
@@ -51,7 +64,6 @@ private:
void* libHandle_ = nullptr;
void* vrSystem_ = nullptr;
void* vrOverlay_ = nullptr;
uint64_t inputOverlay_ = 0;
};
} // namespace pixelvr
+68
View File
@@ -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<int>(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
+8
View File
@@ -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<uint32_t>(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;