diff --git a/package/Shaders/VR/InSceneOverlay.ps.hlsl b/package/Shaders/VR/InSceneOverlay.ps.hlsl deleted file mode 100644 index e8c01fe231..0000000000 --- a/package/Shaders/VR/InSceneOverlay.ps.hlsl +++ /dev/null @@ -1,17 +0,0 @@ -// VR In-Scene Overlay Pixel Shader -// Samples overlay texture with alpha blending support - -Texture2D shaderTexture : register(t0); -SamplerState sampleType : register(s0); - -struct PS_INPUT -{ - float4 pos : SV_POSITION; - float2 uv : TEXCOORD0; -}; - -float4 main(PS_INPUT input) : SV_TARGET -{ - float4 color = shaderTexture.Sample(sampleType, input.uv); - return color; -} diff --git a/package/Shaders/VR/InSceneOverlay.vs.hlsl b/package/Shaders/VR/InSceneOverlay.vs.hlsl deleted file mode 100644 index ea5cba5d4b..0000000000 --- a/package/Shaders/VR/InSceneOverlay.vs.hlsl +++ /dev/null @@ -1,27 +0,0 @@ -// VR In-Scene Overlay Vertex Shader -// Simple pass-through shader for rendering overlay quad in VR - -cbuffer MatrixBuffer : register(b0) -{ - matrix wvp; -}; - -struct VS_INPUT -{ - float3 pos : POSITION; - float2 uv : TEXCOORD0; -}; - -struct PS_INPUT -{ - float4 pos : SV_POSITION; - float2 uv : TEXCOORD0; -}; - -PS_INPUT main(VS_INPUT input) -{ - PS_INPUT output; - output.pos = mul(float4(input.pos, 1.0f), wvp); - output.uv = input.uv; - return output; -} diff --git a/src/Features/VR.cpp b/src/Features/VR.cpp index 23b547341f..47d4f6a9d7 100644 --- a/src/Features/VR.cpp +++ b/src/Features/VR.cpp @@ -1,21 +1,41 @@ #include "VR.h" #include "FeatureConstraints.h" #include "Menu.h" +#include "Menu/Fonts.h" #include "RE/B/BSOpenVR.h" +#include "RE/N/NiPoint3.h" #include "RE/P/PlayerCharacter.h" #include "Upscaling.h" -#include "VR/OpenVRDetection.h" +#include #include "State.h" #include "Utils/D3D.h" +#include "Utils/PerfUtils.h" +#include "Utils/UI.h" #include "Utils/VRUtils.h" - +#include +#include +#include #include #include -#include +#include +#include +#include +#pragma comment(lib, "version.lib") using AttachMode = VR::Settings::OverlayAttachMode; +namespace +{ + bool BeginTabItemWithFont(const char* label, Menu::FontRole role, ImGuiTabItemFlags flags = ImGuiTabItemFlags_None) + { + return MenuFonts::BeginTabItemWithFont(label, role, flags); + } +} + +constexpr int kOverlayWidth = 1920; +constexpr int kOverlayHeight = 1080; + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( VR::Settings, EnableDepthBufferCullingInterior, @@ -51,6 +71,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void VR::LoadSettings(json& o_json) { settings = o_json.get(); + // Validate and clamp loaded settings to ensure they're within valid ranges settings.ClampToValidRanges(); } @@ -66,27 +87,21 @@ void VR::RestoreDefaultSettings() void VR::SetupResources() { + // Detect OpenVR version and compatibility early to avoid CTDs DetectOpenVRInfo(); + // Log OpenVR information if (openVRInfo.isAvailable) { logger::info("OpenVR DLL detected:"); logger::info(" Path: {}", openVRInfo.dllPath); logger::info(" Version: {}", openVRInfo.version); logger::info(" Size: {} bytes", openVRInfo.fileSize); logger::info(" Modified: {}", openVRInfo.modificationTime); - logger::info(" Runtime: {}", VRDetection::RuntimeTypeToString(openVRInfo.runtimeType)); - logger::info(" Interface probing: {}", openVRInfo.probingSucceeded ? "Passed" : "Failed"); - logger::info(" Overlay (IVROverlay_016): {}", openVRInfo.hasOverlayInterface ? "Yes" : "No"); - logger::info(" System (IVRSystem_017): {}", openVRInfo.hasSystemInterface ? "Yes" : "No"); - logger::info(" Compositor (IVRCompositor_021): {}", openVRInfo.hasCompositorInterface ? "Yes" : "No"); logger::info(" Compatible: {}", openVRInfo.isCompatible ? "Yes" : "No"); if (!openVRInfo.isCompatible) { - if (globals::state->IsDeveloperMode()) { - logger::info("OpenVR not natively compatible, but developer mode is active - VR menus enabled"); - } else { - logger::info("OpenVR version is incompatible. Community Shaders VR menus will be disabled for stability"); - } + logger::info("OpenVR version is incompatible."); + logger::info("Community Shaders VR menus will be disabled for stability"); } } else { logger::info("OpenVR DLL not available in current process"); @@ -97,7 +112,7 @@ void VR::PostPostLoad() { gDepthBufferCulling = reinterpret_cast(REL::Offset(0x1EC6B88).address()); if (!gDepthBufferCulling) { - static bool s_defaultDepthBufferCulling = false; + static bool s_defaultDepthBufferCulling = false; // safe fallback gDepthBufferCulling = &s_defaultDepthBufferCulling; logger::warn("VR: gDepthBufferCulling address not found - using fallback default (false)"); } @@ -109,22 +124,7 @@ void VR::PostPostLoad() logger::warn("VR: gMinOccludeeBoxExtent address not found - using fallback default (10.0)"); } - // Migration: Fix legacy overlay keybinds - if (settings.VROverlayCloseKeys.size() == 1) { - auto& closeKey = settings.VROverlayCloseKeys[0]; - if (closeKey.GetDevice() == ControllerDevice::Keyboard && closeKey.GetKey() == 32) { - settings.VROverlayCloseKeys[0] = InputCombo::Primary(32); - logger::info("VR: Migrated VROverlayCloseKeys from Keyboard(32) to Primary(32)"); - } - } - if (settings.VROverlayOpenKeys.size() == 1) { - auto& openKey = settings.VROverlayOpenKeys[0]; - if (openKey.GetDevice() == ControllerDevice::Keyboard && openKey.GetKey() == 32) { - settings.VROverlayOpenKeys[0] = InputCombo::Secondary(32); - logger::info("VR: Migrated VROverlayOpenKeys from Keyboard(32) to Secondary(32)"); - } - } - + // Patches BSGeometry::CopyTransformAndBounds to copy the model-bound translation across correctly instead of overwriting it with the bounding sphere centre REL::safe_write(REL::RelocationID(0, 0, 69528).address() + REL::Relocate(0, 0, 0xD9) + 0x2, 0x148); REL::safe_write(REL::RelocationID(0, 0, 69528).address() + REL::Relocate(0, 0, 0xE5) + 0x2, 0x14C); REL::safe_write(REL::RelocationID(0, 0, 69528).address() + REL::Relocate(0, 0, 0xF1) + 0x2, 0x150); @@ -132,6 +132,8 @@ void VR::PostPostLoad() void VR::DataLoaded() { + // Initialize occlusion culling based on settings, but force-disable if an external + // upscaler is active (FSR/DLSS) since upscalers may modify the depth buffer. bool desired = settings.EnableDepthBufferCullingExterior; UpdateDepthBufferCulling(desired, { "VR", "EnableDepthBufferCullingExterior" }); @@ -144,6 +146,8 @@ void VR::DataLoaded() void VR::EarlyPrepass() { + // Respect user settings unless an external upscaler is active; if so, force-disable + // depth-buffer culling to avoid incorrect occlusion tests in VR. bool isInterior = RE::TES::GetSingleton()->interiorCell != nullptr; auto settingId = isInterior ? FeatureConstraints::SettingId{ "VR", "EnableDepthBufferCullingInterior" } : FeatureConstraints::SettingId{ "VR", "EnableDepthBufferCullingExterior" }; bool desired = isInterior ? settings.EnableDepthBufferCullingInterior : settings.EnableDepthBufferCullingExterior; @@ -151,99 +155,2500 @@ void VR::EarlyPrepass() } //============================================================================= -// OVERLAY SUBMIT AND DEPTH BUFFER CULLING +// OVERLAY FEATURE OVERRIDES //============================================================================= -void VR::RecreateOverlayTexturesIfNeeded() +void VR::DrawOverlay() { - Util::CreateOverlayTextureAndRTV(globals::d3d::device, Config::kOverlayWidth, Config::kOverlayHeight, menuTexture.put(), menuRTV.put()); + auto& vr = globals::features::vr; + if (!vr.openVRInfo.isCompatible) + return; + static LARGE_INTEGER overlayShowStart = { 0 }; + static LARGE_INTEGER freq = { 0 }; + + bool shouldShow = settings.kAutoHideSeconds > 0 && globals::game::ui && globals::game::ui->IsMenuOpen(RE::MainMenu::MENU_NAME) && globals::menu && !globals::menu->IsEnabled; + + if (!shouldShow) { + overlayShowStart.QuadPart = 0; // Reset timer when overlay is not shown + return; + } + + if (freq.QuadPart == 0) { + QueryPerformanceFrequency(&freq); + } + + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + + if (overlayShowStart.QuadPart == 0) { + overlayShowStart = now; + } + + double elapsed = double(now.QuadPart - overlayShowStart.QuadPart) / double(freq.QuadPart); + const double autoHideSeconds = static_cast(settings.kAutoHideSeconds); + if (elapsed >= autoHideSeconds) { + return; + } + int secondsLeft = int(std::ceil(autoHideSeconds - elapsed)); + + ImGuiIO& io = ImGui::GetIO(); + ImVec2 overlaySize(480, 0); // width, height auto + ImVec2 overlayPos = ImVec2((io.DisplaySize.x - overlaySize.x) * 0.5f, 80.0f); + ImGui::SetNextWindowPos(overlayPos, ImGuiCond_Always); + ImGui::SetNextWindowSize(overlaySize, ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.92f); + + ImGui::Begin("HowToUseOverlay", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav); + ImGui::Text("How to Use VR Community Shaders Menu:"); + ImGui::Separator(); + ImGui::Text("You must be in the Main Menu or Tween Menu for these key binds to work."); + ImGui::Spacing(); + ImGui::Text("Open Menu: "); + Util::DrawButtonCombo(settings.VRMenuOpenKeys, true); + ImGui::Text("\nClose Menu: "); + Util::DrawButtonCombo(settings.VRMenuCloseKeys, true); + ImGui::Spacing(); + ImGui::TextDisabled("(This message will auto-disable in %d seconds)", secondsLeft); + ImGui::TextDisabled("(You can disable this message in VR settings > Controller Input Instructions)"); + ImGui::End(); } -void VR::SubmitOverlayFrame() +namespace { - InstallSubmitHook(); + void DrawControllerInputInstructions(); + void DrawGeneralVRSettings(); + void DrawMenuSettings(); + void DrawMouseSettings(); + void DrawDragSettings(); + void DrawKeyBindings(); + void DrawDebugSection(); +} - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - if (!openvr || !openvr->vrSystem) { +void VR::DrawSettings() +{ + auto menu = globals::menu; + if (!menu) return; + if (ImGui::BeginTabBar("##VRTabs", ImGuiTabBarFlags_None)) { + // General Settings Tab + if (BeginTabItemWithFont("General", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##VRGeneralFrame", { 0, 0 }, true)) { + DrawGeneralVRSettings(); + DrawControllerInputInstructions(); + DrawMenuSettings(); + DrawMouseSettings(); + DrawDragSettings(); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } + + // Key Bindings Tab + if (openVRInfo.isCompatible) { + if (BeginTabItemWithFont("Bindings", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##VRBindingsFrame", { 0, 0 }, true)) { + DrawKeyBindings(); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } + } + // Debug Tab (existing debug functionality) + if (BeginTabItemWithFont("Debug", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##VRDebugFrame", { 0, 0 }, true)) { + DrawDebugSection(); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); } - auto& enabled = globals::menu->IsEnabled; - auto& overlayVisible = globals::menu->overlayVisible; + // Combo recording popup + if (this->isCapturingCombo) { + ImGui::OpenPopup("Record Combo"); + ImGui::SetNextWindowSize(ImVec2(400, 200), ImGuiCond_FirstUseEver); + if (ImGui::BeginPopupModal("Record Combo", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + // Helper function to get button name + auto GetButtonName = [](uint32_t key) -> const char* { + switch (key) { + case static_cast(RE::BSOpenVRControllerDevice::Keys::kTrigger): + return "Trigger"; + case static_cast(RE::BSOpenVRControllerDevice::Keys::kGrip): + return "Grip"; + case static_cast(RE::BSOpenVRControllerDevice::Keys::kTouchpadClick): + return "Touchpad"; + case static_cast(RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger): + return "Stick Click"; + case static_cast(RE::BSOpenVRControllerDevice::Keys::kXA): + return "A/X"; + case static_cast(RE::BSOpenVRControllerDevice::Keys::kBY): + return "B/Y"; + default: + return "Unknown"; + } + }; - if ((enabled || overlayVisible || settings.kAutoHideSeconds > 0) && menuTexture.get() && menuRTV.get()) { - UpdateFixedWorldPositioning(); - UpdateOverlayDrag(); + ImGui::Text("Recording combo for: %s", this->currentComboName ? this->currentComboName : "Unknown"); + ImGui::Spacing(); - ID3D11RenderTargetView* oldRTV = nullptr; - globals::d3d::context->OMGetRenderTargets(1, &oldRTV, nullptr); - ID3D11RenderTargetView* menuRTVPtr = menuRTV.get(); - globals::d3d::context->OMSetRenderTargets(1, &menuRTVPtr, nullptr); - float clearColor[4] = { 0, 0, 0, 0 }; - globals::d3d::context->ClearRenderTargetView(menuRTV.get(), clearColor); - ImGui::Render(); - ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); - globals::d3d::context->OMSetRenderTargets(1, &oldRTV, nullptr); + ImGui::TextDisabled("(During recording, any controller's buttons can be used. Requirement is only enforced during use.)"); - bool beingDragged = settings.EnableDragToReposition && overlayDragState.dragging; - Util::ApplyHighlightTintToTexture(menuTexture.get(), beingDragged, settings.dragHighlightColor); + ImGui::Spacing(); - if (oldRTV) - oldRTV->Release(); + // Show countdown timer with color + double remainingTime = this->comboTimeout - (Util::GetNowSecs() - this->comboStartTime); + ImVec4 timerColor = remainingTime > 2.0 ? Util::Colors::GetTimerGood() : + remainingTime > 1.0 ? Util::Colors::GetTimerWarning() : + Util::Colors::GetTimerCritical(); + ImGui::TextColored(timerColor, "Time remaining: %.1f seconds", remainingTime); + + ImGui::Spacing(); + + // Show recorded buttons + if (this->recordedCombo.empty()) { + ImGui::Text("Press buttons to record combo..."); + } else { + ImGui::Text("Recorded buttons:"); + // Create a sorted list of decoded buttons for consistent display + std::vector sortedRecordedCombos; + for (size_t i = 0; i < this->recordedCombo.size(); ++i) { + sortedRecordedCombos.push_back(this->recordedCombo[i]); + } + std::sort(sortedRecordedCombos.begin(), sortedRecordedCombos.end(), + [](const ButtonCombo& a, const ButtonCombo& b) { + return a.GetKey() < b.GetKey(); + }); + + Util::DrawButtonCombo(sortedRecordedCombos, false); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Instructions + ImGui::Text("Press ENTER to accept, ESC to cancel"); + + // Handle button recording + // Check for VR controller button presses - record them (any controller allowed during recording) + bool buttonPressed = false; + uint32_t pressedKey = 0; + ControllerDevice pressedDevice = ControllerDevice::Both; // Default to Both, will set below + + // Check primary controller buttons + for (const auto& [keyCode, buttonState] : primaryControllerState.GetActiveButtons()) { + if (buttonState->isPressed) { + pressedKey = keyCode; + buttonPressed = true; + pressedDevice = ControllerDevice::Primary; + break; + } + } + + // Check secondary controller buttons if primary didn't have any + if (!buttonPressed) { + for (const auto& [keyCode, buttonState] : secondaryControllerState.GetActiveButtons()) { + if (buttonState->isPressed) { + pressedKey = keyCode; + buttonPressed = true; + pressedDevice = ControllerDevice::Secondary; + break; + } + } + } + + // Record button press + if (buttonPressed) { + // Check if this button is already in the combo (avoid duplicates) + auto it = recordingButtonControllers.find(pressedKey); + if (it == recordingButtonControllers.end()) { + // Not yet recorded, add with the current device + recordingButtonControllers[pressedKey] = pressedDevice; + } else { + // Already recorded, if the other controller is now pressed, set to BOTH + if (it->second != pressedDevice && it->second != ControllerDevice::Both) { + it->second = ControllerDevice::Both; + } + } + // Update the recordedCombo vector to match the map + this->recordedCombo.clear(); + for (const auto& [key, device] : recordingButtonControllers) { + this->recordedCombo.push_back(ButtonCombo(device, key)); + } + } + + // Handle ENTER key to accept combo + if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter)) || ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_KeypadEnter))) { + if (!this->recordedCombo.empty()) { + // Apply the recorded combo to the correct settings vector + switch (this->currentComboType) { + case VR::ComboType::MenuOpen: + settings.VRMenuOpenKeys = this->recordedCombo; + break; + case VR::ComboType::MenuClose: + settings.VRMenuCloseKeys = this->recordedCombo; + break; + case VR::ComboType::OverlayOpen: + settings.VROverlayOpenKeys = this->recordedCombo; + break; + case VR::ComboType::OverlayClose: + settings.VROverlayCloseKeys = this->recordedCombo; + break; + default: + break; + } + } + + // Reset recording state + this->isCapturingCombo = false; + this->currentComboType = VR::ComboType::None; + this->currentComboName = nullptr; + this->recordedCombo.clear(); + this->comboStartTime = 0.0; + recordingButtonControllers.clear(); + ImGui::CloseCurrentPopup(); + } + + // Handle ESC key to cancel + if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape))) { + // Reset recording state + this->isCapturingCombo = false; + this->currentComboType = VR::ComboType::None; + this->currentComboName = nullptr; + this->recordedCombo.clear(); + this->comboStartTime = 0.0; + recordingButtonControllers.clear(); + ImGui::CloseCurrentPopup(); + } + + // Handle timeout - auto-accept if buttons were pressed, auto-cancel if not + if (remainingTime <= 0.0) { + if (!this->recordedCombo.empty()) { + // Auto-accept if buttons were pressed - apply to correct settings vector + switch (this->currentComboType) { + case VR::ComboType::MenuOpen: + settings.VRMenuOpenKeys = this->recordedCombo; + break; + case VR::ComboType::MenuClose: + settings.VRMenuCloseKeys = this->recordedCombo; + break; + case VR::ComboType::OverlayOpen: + settings.VROverlayOpenKeys = this->recordedCombo; + break; + case VR::ComboType::OverlayClose: + settings.VROverlayCloseKeys = this->recordedCombo; + break; + default: + break; + } + } + // Auto-cancel if no buttons were pressed (do nothing, just close) + + // Reset recording state + this->isCapturingCombo = false; + this->currentComboType = VR::ComboType::None; + this->currentComboName = nullptr; + this->recordedCombo.clear(); + this->comboStartTime = 0.0; + recordingButtonControllers.clear(); + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } } } -void VR::UpdateDepthBufferCulling(bool desired, const FeatureConstraints::SettingId& settingId) +namespace { - auto constraint = FeatureConstraints::GetConstraints(settingId); + void DrawControllerInputInstructions() + { + auto& vr = globals::features::vr; + auto& settings = vr.settings; + if (!vr.openVRInfo.isCompatible) + return; + if (ImGui::CollapsingHeader("Controller Input Instructions", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderInt("Auto-hide Welcome overlay timeout", &settings.kAutoHideSeconds, 0, VR::Config::kMaxAutoHideSeconds, + settings.kAutoHideSeconds <= 0 ? "Hidden" : "%d seconds"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Set to 0 to hide the overlay, or a positive value to show it for that many seconds"); + } + ImGui::TextWrapped("Menu (while in the main menu or tween menu):"); + if (ImGui::BeginTable("MenuInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Open Community Shaders Menu:"); + ImGui::TableSetColumnIndex(1); + Util::DrawButtonCombo(settings.VRMenuOpenKeys, true); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Close Community Shaders Menu:"); + ImGui::TableSetColumnIndex(1); + Util::DrawButtonCombo(settings.VRMenuCloseKeys, true); + ImGui::EndTable(); + } + ImGui::TextWrapped("Overlay (while in the main menu or tween menu):"); + if (ImGui::BeginTable("OverlayInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Open Overlay:"); + ImGui::TableSetColumnIndex(1); + Util::DrawButtonCombo(settings.VROverlayOpenKeys, true); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Close Overlay:"); + ImGui::TableSetColumnIndex(1); + Util::DrawButtonCombo(settings.VROverlayCloseKeys, true); + ImGui::EndTable(); + } + ImGui::TextWrapped("Menu Controller Input:"); + if (ImGui::BeginTable("ControllerInputTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerBothColor(), "Trigger (Both Controllers)"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Left mouse button"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerBothColor(), "Grip (Both Controllers)"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Right mouse button"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerBothColor(), "Touchpad Click (Both Controllers)"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Middle mouse button"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerBothColor(), "Stick Click (Both Controllers)"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Middle mouse button"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerBothColor(), "A/X (Both Controllers)"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Enter"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerPrimaryColor(), "B/Y (Primary Controller)"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Tab"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerSecondaryColor(), "B/Y (Secondary Controller)"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Shift+Tab"); + ImGui::EndTable(); + } + // Thumbstick instructions + bool useAttachedControllerForCursor = (settings.attachMode == VR::Settings::OverlayAttachMode::ControllerOnly || settings.attachMode == VR::Settings::OverlayAttachMode::Both); + if (ImGui::BeginTable("ThumbstickInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (useAttachedControllerForCursor) { + if (settings.VRMenuAttachController == ControllerDevice::Primary) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Mouse movement (attached controller)"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Scroll"); + } else { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Scroll"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Mouse movement (attached controller)"); + } + } else { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Mouse movement (HMD mode)"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Scroll"); + } + ImGui::EndTable(); + } + } + } - if (constraint.isConstrained) { - if (auto* forcedValuePtr = std::get_if(&constraint.forcedValue)) { - bool forcedValue = *forcedValuePtr; - if (gDepthBufferCulling && *gDepthBufferCulling != forcedValue) { - *gDepthBufferCulling = forcedValue; - for (const auto& src : constraint.sources) { - logger::info("{} forcing depth buffer culling {}: {}", - src.featureName, - forcedValue ? "ON" : "OFF", - src.reason); + void DrawGeneralVRSettings() + { + auto& vr = globals::features::vr; + VR::Settings& settings = vr.settings; + if (ImGui::CollapsingHeader("General Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + // Use constraint-aware checkboxes that automatically handle disabling + // and showing tooltips when other features constrain these settings + Util::ConstrainedUI::Checkbox("Enable Depth Buffer Culling in Exteriors", + &settings.EnableDepthBufferCullingExterior, + { "VR", "EnableDepthBufferCullingExterior" }); + // Show normal tooltip when not constrained + auto exteriorConstraint = FeatureConstraints::GetConstraints({ "VR", "EnableDepthBufferCullingExterior" }); + if (!exteriorConstraint.isConstrained) { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Improves performance in exteriors, recommended ON."); } } - } else { - logger::warn("VR::UpdateDepthBufferCulling: Constraint on {} has non-bool forced value, ignoring", settingId.settingPath); + + Util::ConstrainedUI::Checkbox("Enable Depth Buffer Culling in Interiors", + &settings.EnableDepthBufferCullingInterior, + { "VR", "EnableDepthBufferCullingInterior" }); + // Show normal tooltip when not constrained + auto interiorConstraint = FeatureConstraints::GetConstraints({ "VR", "EnableDepthBufferCullingInterior" }); + if (!interiorConstraint.isConstrained) { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Improves performance in interiors, recommended OFF due to occasional visual glitches."); + } + } + + if (ImGui::SliderFloat("Min Occludee Box Extent", &settings.MinOccludeeBoxExtent, 0.0f, 1000.0f, "%.1f")) + *vr.gMinOccludeeBoxExtent = settings.MinOccludeeBoxExtent; + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Minimum bounding box dimensions for object occlusion culling. Lower values improve performance but may result in visual artifacts."); + } } - } else { - if (gDepthBufferCulling && *gDepthBufferCulling != desired) { - *gDepthBufferCulling = desired; - logger::info("VR depth buffer culling set to {}", desired); + } + + void DrawMenuSettings() + { + auto& vr = globals::features::vr; + auto& settings = vr.settings; + if (!vr.openVRInfo.isCompatible) + return; + if (ImGui::CollapsingHeader("Menu Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderFloat("Menu Scale", &settings.VRMenuScale, VR::Config::kMinMenuScale, VR::Config::kMaxMenuScale, "%.2f"); + const char* positioningMethods[] = { "HMD Relative", "Fixed World Position" }; + ImGui::Combo("Menu Positioning Method", &settings.VRMenuPositioningMethod, positioningMethods, IM_ARRAYSIZE(positioningMethods)); + const char* attachModes[] = { "HMD Only", "Controller Only", "Both" }; + int attachModeInt = static_cast(settings.attachMode); + if (ImGui::Combo("Attach Mode", &attachModeInt, attachModes, IM_ARRAYSIZE(attachModes))) { + settings.attachMode = static_cast(attachModeInt); + } + + // Controller-specific settings (only show when controller mode is active) + if (settings.attachMode == VR::Settings::OverlayAttachMode::ControllerOnly || + settings.attachMode == VR::Settings::OverlayAttachMode::Both) { + const char* attachControllers[] = { "Primary Controller", "Secondary Controller" }; + int attachControllerInt = static_cast(settings.VRMenuAttachController); + if (ImGui::Combo("Attach to Controller", &attachControllerInt, attachControllers, IM_ARRAYSIZE(attachControllers))) { + settings.VRMenuAttachController = static_cast(attachControllerInt); + } + + ImGui::Separator(); + ImGui::Text("Controller Offset Settings"); + ImGui::SliderFloat("Controller Offset X", &settings.VRMenuControllerOffsetX, -2.0f, 2.0f, "%.2f"); + ImGui::SliderFloat("Controller Offset Y", &settings.VRMenuControllerOffsetY, -2.0f, 2.0f, "%.2f"); + ImGui::SliderFloat("Controller Offset Z", &settings.VRMenuControllerOffsetZ, -2.0f, 2.0f, "%.2f"); + } + + // HMD-specific settings (only show when HMD mode is active) + if (settings.attachMode == VR::Settings::OverlayAttachMode::HMDOnly || + settings.attachMode == VR::Settings::OverlayAttachMode::Both) { + ImGui::Separator(); + ImGui::Text("HMD Offset Settings"); + ImGui::SliderFloat("HMD Offset X", &settings.VRMenuOffsetX, -2.0f, 2.0f, "%.2f"); + ImGui::SliderFloat("HMD Offset Y", &settings.VRMenuOffsetY, -2.0f, 2.0f, "%.2f"); + ImGui::SliderFloat("HMD Offset Z", &settings.VRMenuOffsetZ, -2.0f, 2.0f, "%.2f"); + } + + // Fixed World Position: show auto reset distance and manual reset button + if (settings.VRMenuPositioningMethod == 1) { // 1 = Fixed World Position + ImGui::Separator(); + ImGui::Text("Fixed World Position Settings"); + ImGui::SliderFloat("Auto Reset Distance (game units)", &settings.VRMenuAutoResetDistance, 100.0f, 5000.0f, "%.0f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("If you move farther than this distance from the menu, it will automatically reset to your HMD position. %s", Util::Units::FormatDistance(settings.VRMenuAutoResetDistance).c_str()); + } + if (ImGui::Button("Reset Menu to HMD Position")) { + vr.SetFixedOverlayToCurrentHMD(); + } + } } } -} + + void DrawMouseSettings() + { + auto& vr = globals::features::vr; + if (!vr.openVRInfo.isCompatible) + return; + VR::Settings& settings = vr.settings; + if (ImGui::CollapsingHeader("Input Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + // Wand pointing settings + if (ImGui::Checkbox("Enable Wand Pointing", &settings.EnableWandPointing)) { + // Reset wand state when toggling + vr.wandState.isIntersecting = false; + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Use controller ray-casting to point at UI elements"); + } + ImGui::Separator(); + ImGui::Text("Joystick Settings"); + ImGui::SliderFloat("Mouse Deadzone", &settings.mouseDeadzone, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Thumbstick deadzone for joystick cursor movement"); + } + ImGui::SliderFloat("Mouse Speed", &settings.mouseSpeed, 0.1f, 50.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Speed multiplier for joystick cursor movement"); + } + } + } + + void DrawDragSettings() + { + auto& vr = globals::features::vr; + if (!vr.openVRInfo.isCompatible) + return; + VR::Settings& settings = vr.settings; + if (ImGui::CollapsingHeader("Drag Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::CollapsingHeader("Drag Instructions", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::TextWrapped("Overlay Positioning (Grip + Drag):"); + ImGui::BulletText("Fixed World Position: Any controller can drag (HMD-only mode) or attached controller only (Both modes)"); + ImGui::BulletText("HMD Relative: Any controller can drag (HMD-only mode) or attached controller only (Both modes)"); + ImGui::BulletText("Controller Attached: Only the opposite hand can drag the controller overlay"); + } + ImGui::Checkbox("Enable drag to reposition overlays", &settings.EnableDragToReposition); + ImGui::BeginDisabled(!settings.EnableDragToReposition); + ImGui::ColorEdit4("Drag Highlight Color", settings.dragHighlightColor.data()); + ImGui::EndDisabled(); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Color used to highlight draggable overlays in VR."); + } + } + } + void DrawKeyBindings() + { + auto& vr = globals::features::vr; + auto& settings = vr.settings; + + // Combo Settings + if (ImGui::CollapsingHeader("Combo Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderFloat("Combo Timeout", &settings.comboTimeout, 1.0f, 10.0f, "%.1f seconds"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Time limit for recording button combinations."); + } + } + ImGui::Separator(); + // Combo box for selecting which combo to record + const char* comboTypes[] = { + "Open Community Shaders Menu", + "Close Community Shaders Menu", + "Open VR Overlay", + "Close VR Overlay" + }; + static int selectedComboIndex = 0; + ImGui::Text("Select Combo to Record:"); + ImGui::SameLine(); + if (ImGui::Combo("##ComboSelector", &selectedComboIndex, comboTypes, IM_ARRAYSIZE(comboTypes))) { + // Reset recording state when changing selection + vr.isCapturingCombo = false; + vr.currentComboType = VR::ComboType::None; + vr.recordedCombo.clear(); + } + if (ImGui::Button("Record Selected Combo")) { + // Start recording the selected combo + vr.isCapturingCombo = true; + vr.currentComboType = static_cast(selectedComboIndex + 1); + vr.currentComboName = comboTypes[selectedComboIndex]; + vr.recordedCombo.clear(); + vr.comboStartTime = Util::GetNowSecs(); + vr.recordingButtonControllers.clear(); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Clear")) { + // Clear the selected combo + switch (selectedComboIndex) { + case 0: + settings.VRMenuOpenKeys.clear(); + break; + case 1: + settings.VRMenuCloseKeys.clear(); + break; + case 2: + settings.VROverlayOpenKeys.clear(); + break; + case 3: + settings.VROverlayCloseKeys.clear(); + break; + } + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Click to start recording a new button combination for the selected action."); + } + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + // Table for displaying current key bindings + if (ImGui::BeginTable("##VRBindingsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("Action"); + ImGui::TableSetupColumn("Current Binding"); + ImGui::TableSetupColumn("Description"); + ImGui::TableHeadersRow(); + // Define VR key binding configurations + struct VRKeyBindingConfig + { + const char* label; + std::vector& combos; + const char* description; + const char* controllerRequirement; + }; + std::vector keyBindingConfigs = { + { "Open Community Shaders Menu", settings.VRMenuOpenKeys, "Button combination to open the Community Shaders menu", "Primary" }, + { "Close Community Shaders Menu", settings.VRMenuCloseKeys, "Button combination to close the Community Shaders menu", "Both" }, + { "Open VR Overlay", settings.VROverlayOpenKeys, "Button combination to open the VR overlay", "Primary" }, + { "Close VR Overlay", settings.VROverlayCloseKeys, "Button combination to close the VR overlay", "Secondary" } + }; + for (size_t row = 0; row < keyBindingConfigs.size(); ++row) { + const auto& config = keyBindingConfigs[row]; + ImGui::TableNextRow(); + // Highlight the selected row + if (row == static_cast(selectedComboIndex)) { + ImU32 highlight = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 0.0f, 0.15f)); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, highlight); + } + // Make row selectable + ImGui::TableSetColumnIndex(0); + char selectableId[64]; + snprintf(selectableId, sizeof(selectableId), "##combo_row_%zu", row); + bool rowSelected = (row == static_cast(selectedComboIndex)); + if (ImGui::Selectable(selectableId, rowSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap, ImVec2(0, 0))) { + selectedComboIndex = static_cast(row); + } + ImGui::SameLine(0, 0); + ImGui::Text("%s", config.label); + // Current Binding column + ImGui::TableSetColumnIndex(1); + Util::DrawButtonCombo(config.combos, false); + // Description column + ImGui::TableSetColumnIndex(2); + ImGui::Text("%s", config.description); + } + ImGui::EndTable(); + } + ImGui::Spacing(); + // Reset to defaults button + if (ImGui::Button("Reset to Defaults")) { + // Use InputCombo structure for cleaner defaults + settings.VRMenuOpenKeys = { + InputCombo::Primary(static_cast(RE::BSOpenVRControllerDevice::Keys::kXA)), + InputCombo::Primary(static_cast(RE::BSOpenVRControllerDevice::Keys::kBY)) + }; + settings.VRMenuCloseKeys = { + InputCombo::Both(static_cast(RE::BSOpenVRControllerDevice::Keys::kGrip)) + }; + settings.VROverlayOpenKeys = { + InputCombo::Primary(static_cast(RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger)) + }; + settings.VROverlayCloseKeys = { + InputCombo::Secondary(static_cast(RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger)) + }; + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Reset all VR key bindings to their default values."); + } + } + void DrawDebugSection() + { + auto& vr = globals::features::vr; + auto& settings = vr.settings; + auto menu = globals::menu; + + // OpenVR Version Information + if (ImGui::CollapsingHeader("OpenVR Information", ImGuiTreeNodeFlags_DefaultOpen)) { + auto& info = vr.openVRInfo; + if (info.isAvailable) { + ImGui::Text("OpenVR System: %s", info.isCompatible ? "Active & Compatible" : "Active but INCOMPATIBLE"); + if (!info.isCompatible) { + std::string reason = std::format("{} {}", "OpenVR version is incompatible.", "VR menus disabled."); + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "Reason: %s", reason.c_str()); + } + ImGui::Text("DLL Path: %s", info.dllPath.c_str()); + ImGui::Text("DLL Version: %s", info.version.c_str()); + ImGui::Text("DLL Size: %llu bytes", info.fileSize); + ImGui::Text("Modified: %s", info.modificationTime.c_str()); + } else { + ImGui::Text("OpenVR system not available"); + } + } + + // Controller Diagnostics Section + if (ImGui::CollapsingHeader("Controller Diagnostics", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::Checkbox("Test Mode: Disable controller menu input (except scroll controller and triggers)", &settings.VRMenuControllerDiagnosticsTestMode)) { + ImGui::SetScrollHereY(0.0f); // Scroll to top of the window when toggled + } + ImGui::SeparatorText("Button State"); + double nowSecs = Util::GetNowSecs(); + // Get highlight color from theme + ImVec4 highlightColor = menu->GetTheme().StatusPalette.InfoColor; + ImU32 highlightColorU32 = ImGui::ColorConvertFloat4ToU32(highlightColor); + + // Determine display order based on handedness + bool isLeftHanded = vr.lastKnownLeftHandedMode; // Use cached handedness + + if (ImGui::BeginTable("vr_input_state_table", 7, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Button"); + if (isLeftHanded) { + // Left-handed: Primary (left hand) on left, Secondary (right hand) on right + ImGui::TableSetupColumn("Primary State"); + ImGui::TableSetupColumn("Primary Held (s)"); + ImGui::TableSetupColumn("Primary Type"); + ImGui::TableSetupColumn("Secondary State"); + ImGui::TableSetupColumn("Secondary Held (s)"); + ImGui::TableSetupColumn("Secondary Type"); + } else { + // Right-handed: Secondary (left hand) on left, Primary (right hand) on right + ImGui::TableSetupColumn("Secondary State"); + ImGui::TableSetupColumn("Secondary Held (s)"); + ImGui::TableSetupColumn("Secondary Type"); + ImGui::TableSetupColumn("Primary State"); + ImGui::TableSetupColumn("Primary Held (s)"); + ImGui::TableSetupColumn("Primary Type"); + } + ImGui::TableHeadersRow(); + // Helper for button type text + auto DrawButtonType = [](const RE::ButtonState& state) { + if (!state.isPressed) { + if (state.IsClick()) + ImGui::TextUnformatted("Click"); + else if (state.IsHold()) + ImGui::TextUnformatted("Hold"); + else + ImGui::TextUnformatted("-"); + } else { + ImGui::TextUnformatted("Held"); + } + }; + // Helper for printing a row with left/right cell highlight + auto printRow = [&](const char* label, const RE::ButtonState& left, const RE::ButtonState& right) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(label); + ImGui::TableSetColumnIndex(1); + if (left.isPressed) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); + ImGui::TextUnformatted(left.isPressed ? "Pressed" : "Released"); + ImGui::TableSetColumnIndex(2); + if (left.isPressed) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); + ImGui::Text("%.2f", left.GetCurrentHeldTime(nowSecs)); + ImGui::TableSetColumnIndex(3); + if (left.isPressed) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); + DrawButtonType(left); + ImGui::TableSetColumnIndex(4); + if (right.isPressed) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); + ImGui::TextUnformatted(right.isPressed ? "Pressed" : "Released"); + ImGui::TableSetColumnIndex(5); + if (right.isPressed) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); + ImGui::Text("%.2f", right.GetCurrentHeldTime(nowSecs)); + ImGui::TableSetColumnIndex(6); + if (right.isPressed) + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); + DrawButtonType(right); + }; + + // Helper to determine the correct order for display based on handedness + auto printRowWithHandedness = [&](const char* label, auto key) { + auto& primary = vr.primaryControllerState[key]; + auto& secondary = vr.secondaryControllerState[key]; + if (isLeftHanded) { + // Left-handed: Primary (left hand) on left, Secondary (right hand) on right + printRow(label, primary, secondary); + } else { + // Right-handed: Secondary (left hand) on left, Primary (right hand) on right + printRow(label, secondary, primary); + } + }; + + printRowWithHandedness("Trigger", RE::BSOpenVRControllerDevice::Keys::kTrigger); + printRowWithHandedness("Grip", RE::BSOpenVRControllerDevice::Keys::kGrip); + printRowWithHandedness("GripAlt", RE::BSOpenVRControllerDevice::Keys::kGripAlt); + printRowWithHandedness("Stick Click", RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger); + printRowWithHandedness("Touchpad Click", RE::BSOpenVRControllerDevice::Keys::kTouchpadClick); + printRowWithHandedness("Touchpad Alt", RE::BSOpenVRControllerDevice::Keys::kTouchpadAlt); + printRowWithHandedness("B/Y", RE::BSOpenVRControllerDevice::Keys::kBY); + printRowWithHandedness("A/X", RE::BSOpenVRControllerDevice::Keys::kXA); + ImGui::EndTable(); + } + ImGui::SeparatorText("VR Thumbstick State"); + // Helper to draw a thumbstick quadrant visualization (returns ImVec2 for label alignment) + auto DrawThumbstickPad = [&](float x, float y, ImU32 highlightCol) -> ImVec2 { + ImVec2 padSize = ImVec2(80, 80); + ImVec2 cursor = ImGui::GetCursorScreenPos(); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImVec2 center = ImVec2(cursor.x + padSize.x / 2, cursor.y + padSize.y / 2); + float radius = padSize.x / 2 - 4; + ImU32 borderCol = ImGui::GetColorU32(ImGuiCol_Border); + ImU32 axisCol = ImGui::GetColorU32(ImGuiCol_TextDisabled); + ImU32 dotCol = ImGui::GetColorU32(ImGuiCol_Text); + // Draw background + drawList->AddRectFilled(cursor, ImVec2(cursor.x + padSize.x, cursor.y + padSize.y), ImGui::GetColorU32(ImGuiCol_FrameBg)); + // Draw border + drawList->AddRect(cursor, ImVec2(cursor.x + padSize.x, cursor.y + padSize.y), borderCol, 4.0f, 0, 2.0f); + // Draw axes + drawList->AddLine(ImVec2(center.x, cursor.y + 4), ImVec2(center.x, cursor.y + padSize.y - 4), axisCol, 1.0f); + drawList->AddLine(ImVec2(cursor.x + 4, center.y), ImVec2(cursor.x + padSize.x - 4, center.y), axisCol, 1.0f); + // Determine quadrant + int quad = 0; + if (x > 0 && y > 0) + quad = 1; // top-right + else if (x < 0 && y > 0) + quad = 2; // top-left + else if (x < 0 && y < 0) + quad = 3; // bottom-left + else if (x > 0 && y < 0) + quad = 4; // bottom-right + // Highlight quadrant + if (quad != 0) { + ImVec2 q0 = center; + ImVec2 q1 = center; + ImVec2 q2 = center; + ImVec2 q3 = center; + if (quad == 1) { // top-right + q1.x += radius; + q1.y -= radius; + q2.x += radius; + q2.y += 0; + q3.x += 0; + q3.y -= radius; + } else if (quad == 2) { // top-left + q1.x -= radius; + q1.y -= radius; + q2.x -= radius; + q2.y += 0; + q3.x += 0; + q3.y -= radius; + } else if (quad == 3) { // bottom-left + q1.x -= radius; + q1.y += radius; + q2.x -= radius; + q2.y += 0; + q3.x += 0; + q3.y += radius; + } else if (quad == 4) { // bottom-right + q1.x += radius; + q1.y += radius; + q2.x += radius; + q2.y += 0; + q3.x += 0; + q3.y += radius; + } + ImVec2 poly[4] = { center, q1, q2, q3 }; + drawList->AddConvexPolyFilled(poly, 4, highlightCol); + } + // Draw stick position dot + ImVec2 dot = ImVec2(center.x + x * radius, center.y - y * radius); + drawList->AddCircleFilled(dot, 5.0f, dotCol); + // Return size for label alignment + return padSize; + }; + ImU32 highlightCol = ImGui::ColorConvertFloat4ToU32(menu->GetTheme().StatusPalette.InfoColor); + if (ImGui::BeginTable("##VRThumbstickTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) { + if (isLeftHanded) { + // Left-handed: Primary (left hand) on left, Secondary (right hand) on right + ImGui::TableSetupColumn("Primary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); + ImGui::TableSetupColumn("Secondary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); + } else { + // Right-handed: Secondary (left hand) on left, Primary (right hand) on right + ImGui::TableSetupColumn("Secondary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); + ImGui::TableSetupColumn("Primary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); + } + ImGui::TableHeadersRow(); + + // Left column content + ImGui::TableSetColumnIndex(0); + ImGui::BeginGroup(); + if (isLeftHanded) { + // Left-handed: Show primary controller in left column + ImVec2 padSizeL = DrawThumbstickPad(vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x, vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y, highlightCol); + ImGui::Dummy(padSizeL); + ImGui::SetNextItemWidth(160.0f); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetTextLineHeight()); + ImGui::Text("X: %+1.3f Y: %+1.3f [%s]", vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x, vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y, RE::GetQuadrantName(vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x, vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y)); + } else { + // Right-handed: Show secondary controller in left column + ImVec2 padSizeL = DrawThumbstickPad(vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x, vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y, highlightCol); + ImGui::Dummy(padSizeL); + ImGui::SetNextItemWidth(160.0f); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetTextLineHeight()); + ImGui::Text("X: %+1.3f Y: %+1.3f [%s]", vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x, vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y, RE::GetQuadrantName(vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x, vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y)); + } + ImGui::EndGroup(); + + // Right column content + ImGui::TableSetColumnIndex(1); + ImGui::BeginGroup(); + if (isLeftHanded) { + // Left-handed: Show secondary controller in right column + ImVec2 padSizeR = DrawThumbstickPad(vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x, vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y, highlightCol); + ImGui::Dummy(padSizeR); + ImGui::SetNextItemWidth(160.0f); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetTextLineHeight()); + ImGui::Text("X: %+1.3f Y: %+1.3f [%s]", vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x, vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y, RE::GetQuadrantName(vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x, vr.secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y)); + } else { + // Right-handed: Show primary controller in right column + ImVec2 padSizeR = DrawThumbstickPad(vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x, vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y, highlightCol); + ImGui::Dummy(padSizeR); + ImGui::SetNextItemWidth(160.0f); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetTextLineHeight()); + ImGui::Text("X: %+1.3f Y: %+1.3f [%s]", vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x, vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y, RE::GetQuadrantName(vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x, vr.primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y)); + } + ImGui::EndGroup(); + ImGui::EndTable(); + } + ImGui::SeparatorText("Recent VR Controller Events"); + ImGui::TextDisabled("Note: For thumbstick events, KeyCode/Value columns show X/Y floats."); + if (ImGui::BeginTable("eventlog", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("Device", ImGuiTableColumnFlags_WidthFixed, 60.0f); + ImGui::TableSetupColumn("KeyCode/X", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("Value/Y", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("Pressed", ImGuiTableColumnFlags_WidthFixed, 70.0f); + ImGui::TableSetupColumn("Known Mapping", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("Event Type", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableHeadersRow(); + for (const auto& e : vr.vrControllerEventLog) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%d", e.device); + ImGui::TableSetColumnIndex(1); + if (e.heldSource == "thumbstick") { + ImGui::Text("%.3f", e.thumbstickX); + } else { + ImGui::Text("%d", e.keyCode); + } + ImGui::TableSetColumnIndex(2); + if (e.heldSource == "thumbstick") { + ImGui::Text("%.3f", e.thumbstickY); + } else { + ImGui::Text("%d", e.value); + } + ImGui::TableSetColumnIndex(3); + ImGui::Text("%s", e.pressed ? "Pressed" : "Released"); + ImGui::TableSetColumnIndex(4); + if (e.heldSource == "thumbstick") { + ImGui::TextUnformatted(e.controllerRole.c_str()); + } else { + ImGui::TextUnformatted(RE::GetOpenVRButtonName(e.keyCode)); + } + ImGui::TableSetColumnIndex(5); + if (e.heldSource == "thumbstick") { + ImGui::TextUnformatted("-"); + } else { + // Show click/hold for release events if available + if (!e.pressed) { + if (e.heldTime > 0.0) { + if (e.heldTime < 0.5) { + ImGui::Text("Click (%.2fs)", e.heldTime); + } else { + ImGui::Text("Hold (%.2fs)", e.heldTime); + } + } else { + ImGui::Text("Release"); + } + } else if (e.pressed) { + if (e.heldTime > 0.0) { + ImGui::Text("Held for %.2fs", e.heldTime); + } else { + ImGui::Text("Press"); + } + } + } + } + ImGui::EndTable(); + } + + // Wand Pointing Diagnostics + ImGui::SeparatorText("Wand Pointing State"); + if (ImGui::BeginTable("##WandPointingState", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Property", ImGuiTableColumnFlags_WidthFixed, 200.0f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Wand Pointing Enabled"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("%s", settings.EnableWandPointing ? "Yes" : "No"); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Intersecting Overlay"); + ImGui::TableSetColumnIndex(1); + if (vr.wandState.isIntersecting) { + ImGui::TextColored(menu->GetTheme().StatusPalette.InfoColor, "YES"); + } else { + ImGui::Text("No"); + } + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("UV Coordinates"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("(%.3f, %.3f)", vr.wandState.uvCoordinates.x, vr.wandState.uvCoordinates.y); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Controller Index"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("%u", vr.wandState.controllerIndex); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Ray Origin"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("(%.2f, %.2f, %.2f)", vr.wandState.rayOrigin.x, vr.wandState.rayOrigin.y, vr.wandState.rayOrigin.z); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Ray Direction"); + ImGui::TableSetColumnIndex(1); + ImGui::Text("(%.2f, %.2f, %.2f)", vr.wandState.rayDirection.x, vr.wandState.rayDirection.y, vr.wandState.rayDirection.z); + + ImGui::EndTable(); + } + } + + // Debugging addresses for copy/paste + if (ImGui::CollapsingHeader("OpenVR Addresses")) { + auto openvr = RE::BSOpenVR::GetSingleton(); + auto overlay = openvr ? RE::BSOpenVR::GetIVROverlayFromContext(&openvr->vrContext) : nullptr; + auto vrSystem = openvr ? openvr->vrSystem : nullptr; + ADDRESS_NODE(openvr) + ADDRESS_NODE(overlay) + ADDRESS_NODE(vrSystem) + } + } +} // namespace //============================================================================= -// OPENVR VERSION DETECTION AND COMPATIBILITY +// VR-SPECIFIC PUBLIC API //============================================================================= -void VR::DetectOpenVRInfo() +void VR::UpdateVROverlayPosition() { - openVRInfo = {}; + Util::OpenVRContext ctx; + if (!ctx.HasOverlay()) + return; + + if (menuOverlayHandle == vr::k_ulOverlayHandleInvalid) { + return; + } + + // Determine positioning strategy based on settings + bool showOnController = (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both); + bool showOnHMD = (settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both); + + // Texture size + float aspect = static_cast(kOverlayHeight) / kOverlayWidth; + float baseWidth = 1.0f; + float overlayWidth = baseWidth * settings.VRMenuScale; + float overlayHeight = overlayWidth * aspect; + float offsetX = settings.VRMenuOffsetX; + float offsetY = settings.VRMenuOffsetY; + float offsetZ = settings.VRMenuOffsetZ; + + static int lastPositioningMethod = -1; + bool justSwitchedToFixed = (lastPositioningMethod != 1 && settings.VRMenuPositioningMethod == 1); + lastPositioningMethod = settings.VRMenuPositioningMethod; + + // Handle HMD positioning + if (showOnHMD) { + if (settings.VRMenuPositioningMethod == 0) { + // HMD Relative positioning + vr::TrackedDevicePose_t hmdPose; + if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &hmdPose, 1)) + return; + + if (hmdPose.bPoseIsValid) { + // Calculate position in front of HMD using offsets directly + float height = 0.0f; + + // Create transform matrix - start with identity + vr::HmdMatrix34_t hmdTransform; + hmdTransform.m[0][0] = 1.0f; + hmdTransform.m[0][1] = 0.0f; + hmdTransform.m[0][2] = 0.0f; + hmdTransform.m[0][3] = 0.0f; + hmdTransform.m[1][0] = 0.0f; + hmdTransform.m[1][1] = 1.0f; + hmdTransform.m[1][2] = 0.0f; + hmdTransform.m[1][3] = 0.0f; + hmdTransform.m[2][0] = 0.0f; + hmdTransform.m[2][1] = 0.0f; + hmdTransform.m[2][2] = 1.0f; + hmdTransform.m[2][3] = 0.0f; + + // Copy HMD position + hmdTransform.m[0][3] = hmdPose.mDeviceToAbsoluteTracking.m[0][3]; + hmdTransform.m[1][3] = hmdPose.mDeviceToAbsoluteTracking.m[1][3]; + hmdTransform.m[2][3] = hmdPose.mDeviceToAbsoluteTracking.m[2][3]; + + // Copy HMD orientation + hmdTransform.m[0][0] = hmdPose.mDeviceToAbsoluteTracking.m[0][0]; + hmdTransform.m[0][1] = hmdPose.mDeviceToAbsoluteTracking.m[0][1]; + hmdTransform.m[0][2] = hmdPose.mDeviceToAbsoluteTracking.m[0][2]; + hmdTransform.m[1][0] = hmdPose.mDeviceToAbsoluteTracking.m[1][0]; + hmdTransform.m[1][1] = hmdPose.mDeviceToAbsoluteTracking.m[1][1]; + hmdTransform.m[1][2] = hmdPose.mDeviceToAbsoluteTracking.m[1][2]; + hmdTransform.m[2][0] = hmdPose.mDeviceToAbsoluteTracking.m[2][0]; + hmdTransform.m[2][1] = hmdPose.mDeviceToAbsoluteTracking.m[2][1]; + hmdTransform.m[2][2] = hmdPose.mDeviceToAbsoluteTracking.m[2][2]; + + // Apply HMD offset positions directly (in HMD local space) + hmdTransform.m[0][3] += hmdTransform.m[0][0] * offsetX + hmdTransform.m[0][1] * offsetY + hmdTransform.m[0][2] * offsetZ; + hmdTransform.m[1][3] += hmdTransform.m[1][0] * offsetX + hmdTransform.m[1][1] * offsetY + hmdTransform.m[1][2] * offsetZ; + hmdTransform.m[2][3] += hmdTransform.m[2][0] * offsetX + hmdTransform.m[2][1] * offsetY + hmdTransform.m[2][2] * offsetZ; + + // Move up by height (Y axis in HMD space) + hmdTransform.m[0][3] += hmdTransform.m[0][1] * height; + hmdTransform.m[1][3] += hmdTransform.m[1][1] * height; + hmdTransform.m[2][3] += hmdTransform.m[2][1] * height; + + // Scale the overlay based on width/height + hmdTransform.m[0][0] *= overlayWidth; + hmdTransform.m[1][1] *= overlayHeight; + + Util::SetOverlayInputFlags(ctx.overlay, menuOverlayHandle); + ctx.overlay->SetOverlayTransformAbsolute(menuOverlayHandle, vr::TrackingUniverseStanding, &hmdTransform); + ctx.overlay->SetOverlayWidthInMeters(menuOverlayHandle, baseWidth * settings.VRMenuScale); + + } else { + logger::debug("HMD pose invalid, falling back to fixed positioning"); + settings.VRMenuPositioningMethod = 1; // Fall back to fixed positioning + } + } + + if (settings.VRMenuPositioningMethod == 1) { + // Fixed World Position + // Cache player position once per frame + RE::NiPoint3 playerPos = savedPlayerWorldPos; + auto player = RE::PlayerCharacter::GetSingleton(); + if (player) { + playerPos = player->GetPosition(); + } - auto result = VRDetection::Detect(); + if (justSwitchedToFixed) { + SetFixedOverlayToCurrentHMD(); + // Save player position when switching to Fixed World Position + savedPlayerWorldPos = playerPos; + } + + // --- Auto reset logic using player world position --- + float sqDist = playerPos.GetSquaredDistance(savedPlayerWorldPos); + float thresholdSq = settings.VRMenuAutoResetDistance * settings.VRMenuAutoResetDistance; + if (sqDist > thresholdSq) { + SetFixedOverlayToCurrentHMD(); + // Update saved position after reset + savedPlayerWorldPos = playerPos; + } + + // Scale the overlay based on width/height (same as relative HMD mode) + vr::HmdMatrix34_t fixedTransform = Util::MatrixToHmdMatrix34(fixedWorldOverlayPosition.m); + fixedTransform.m[0][0] *= overlayWidth; + fixedTransform.m[1][1] *= overlayHeight; + + Util::SetOverlayInputFlags(ctx.overlay, menuOverlayHandle); + ctx.overlay->SetOverlayTransformAbsolute(menuOverlayHandle, vr::TrackingUniverseStanding, &fixedTransform); + ctx.overlay->SetOverlayWidthInMeters(menuOverlayHandle, baseWidth * settings.VRMenuScale); + } + } + + // Handle controller positioning separately (can be shown alongside HMD) + if (showOnController) { + // Get the VR controller overlay handle from Menu.cpp + if (menuControllerOverlayHandle == vr::k_ulOverlayHandleInvalid) { + return; + } + + // Attach to controller + vr::TrackedDeviceIndex_t controllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + + if (controllerIndex != vr::k_unTrackedDeviceIndexInvalid) { + // Position relative to controller using offset settings + vr::HmdMatrix34_t transform = Util::CreateControllerOverlayTransform( + settings.VRMenuControllerOffsetX, + settings.VRMenuControllerOffsetY, + settings.VRMenuControllerOffsetZ, + overlayWidth, + overlayHeight); + + Util::SetOverlayInputFlags(ctx.overlay, menuControllerOverlayHandle); + ctx.overlay->SetOverlayTransformTrackedDeviceRelative(menuControllerOverlayHandle, controllerIndex, &transform); + + // Update the overlay width to match the calculated size + ctx.overlay->SetOverlayWidthInMeters(menuControllerOverlayHandle, overlayWidth); + + // Update controller overlay flags for input interaction + Util::SetOverlayInputFlags(ctx.overlay, menuControllerOverlayHandle); + } + } - openVRInfo.isAvailable = result.isAvailable; - openVRInfo.isCompatible = result.isCompatible; - openVRInfo.dllPath = result.dllPath; - openVRInfo.version = result.version; - openVRInfo.fileSize = result.fileSize; - openVRInfo.modificationTime = result.modificationTime; - openVRInfo.hasOverlayInterface = result.hasOverlayInterface; - openVRInfo.hasSystemInterface = result.hasSystemInterface; - openVRInfo.hasCompositorInterface = result.hasCompositorInterface; - openVRInfo.runtimeType = result.runtimeType; - openVRInfo.probingSucceeded = result.probingSucceeded; + // Update overlay flags for input interaction + Util::SetOverlayInputFlags(ctx.overlay, menuOverlayHandle); } -bool VR::IsOpenVRCompatible() const +void VR::UpdateVROverlayControllerPosition() +{ + Util::OpenVRContext ctx; + if (!ctx.HasOverlay()) + return; + + // Get the VR controller overlay handle from Menu.cpp + if (menuControllerOverlayHandle == vr::k_ulOverlayHandleInvalid) { + return; + } + + // Texture size based on preset + float aspect = static_cast(kOverlayHeight) / kOverlayWidth; + float baseWidth = 1.0f; + float overlayWidth = baseWidth * settings.VRMenuScale; + float overlayHeight = overlayWidth * aspect; + + // Find the appropriate controller for the controller overlay + vr::TrackedDeviceIndex_t controllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + if (controllerIndex == vr::k_unTrackedDeviceIndexInvalid) { + ctx.overlay->HideOverlay(menuControllerOverlayHandle); + return; + } + + // Position relative to controller using offset settings + vr::HmdMatrix34_t transform = Util::CreateControllerOverlayTransform( + settings.VRMenuControllerOffsetX, + settings.VRMenuControllerOffsetY, + settings.VRMenuControllerOffsetZ, + overlayWidth, + overlayHeight); + + Util::SetOverlayInputFlags(ctx.overlay, menuControllerOverlayHandle); + ctx.overlay->SetOverlayTransformTrackedDeviceRelative(menuControllerOverlayHandle, controllerIndex, &transform); + + // Update the overlay width to match the calculated size + ctx.overlay->SetOverlayWidthInMeters(menuControllerOverlayHandle, overlayWidth); + + // Update controller overlay flags for input interaction + Util::SetOverlayInputFlags(ctx.overlay, menuControllerOverlayHandle); +} + +// Add overlay management methods for VR menu overlays +void VR::EnsureOverlayInitialized() { - return openVRInfo.isCompatible; + // Check OpenVR compatibility first + if (!openVRInfo.isCompatible) { + logger::warn("OpenVR version is incompatible."); + return; + } + + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + logger::debug("BSOpenVR: 0x{:X}", reinterpret_cast(openvr)); + if (!openvr) { + logger::error("BSOpenVR::GetSingleton() returned nullptr"); + return; + } + auto* vrSystem = openvr->vrSystem; + auto* overlay = openvr ? RE::BSOpenVR::GetIVROverlayFromContext(&openvr->vrContext) : nullptr; + logger::debug("openVR->vrSystem: 0x{:X}", reinterpret_cast(vrSystem)); + logger::debug("openVR->vrContext: 0x{:X}", reinterpret_cast(&openvr->vrContext)); + logger::debug("openVR->vrContext.vrOverlay: 0x{:X}", reinterpret_cast(openvr->vrContext.vrOverlay)); + logger::debug("openVR->hmdDeviceType: {} ({})", static_cast(openvr->hmdDeviceType), magic_enum::enum_name(openvr->hmdDeviceType)); + for (int i = 0; i < RE::BSVRInterface::Hand::kTotal; ++i) { + logger::debug("openVR->controllerNodes[{}]: 0x{:X}", i, reinterpret_cast(openvr->controllerNodes[i].get())); + if (openvr->controllerNodes[i] && reinterpret_cast(openvr->controllerNodes[i].get()) < 0x1000) { + logger::warn("controllerNodes[{}] is suspiciously low (0x{:X})", i, reinterpret_cast(openvr->controllerNodes[i].get())); + } + } + logger::debug("menuOverlayHandle: 0x{:X}", menuOverlayHandle); + logger::debug("menuControllerOverlayHandle: 0x{:X}", menuControllerOverlayHandle); + if (!overlay) { + logger::error("IVROverlay is nullptr after GetIVROverlay"); + return; + } + Util::CreateOverlayTextureAndRTV(globals::d3d::device, kOverlayWidth, kOverlayHeight, menuTexture.put(), menuRTV.put()); + std::string key = "communityshaders.menu"; + std::string name = "Community Shaders Menu"; + vr::EVROverlayError err = overlay->CreateOverlay(key.c_str(), name.c_str(), &menuOverlayHandle); + if (err == vr::VROverlayError_None) { + logger::debug("CreateOverlay succeeded for menuOverlayHandle: 0x{:X}", menuOverlayHandle); + Util::SetOverlayInputFlags(overlay, menuOverlayHandle); + overlay->SetOverlayWidthInMeters(menuOverlayHandle, 1.0f); + } else { + logger::error("CreateOverlay failed: {} ({})", static_cast(err), magic_enum::enum_name(err)); + } + // Controller overlay + std::string controllerKey = "communityshaders.menu.controller"; + std::string controllerName = "Community Shaders Menu (Controller)"; + err = overlay->CreateOverlay(controllerKey.c_str(), controllerName.c_str(), &menuControllerOverlayHandle); + if (err == vr::VROverlayError_None) { + logger::debug("CreateOverlay succeeded for menuControllerOverlayHandle: 0x{:X}", menuControllerOverlayHandle); + Util::CreateOverlayTextureAndRTV(globals::d3d::device, kOverlayWidth, kOverlayHeight, menuControllerTexture.put(), menuControllerRTV.put()); + Util::SetOverlayInputFlags(overlay, menuControllerOverlayHandle); + overlay->SetOverlayWidthInMeters(menuControllerOverlayHandle, 1.0f); + } else { + logger::error("CreateOverlay failed: {} ({})", static_cast(err), magic_enum::enum_name(err)); + } +} + +//============================================================================= +// PRIVATE IMPLEMENTATION +//============================================================================= + +void VR::DestroyOverlay() +{ + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + auto* overlay = openvr ? RE::BSOpenVR::GetIVROverlayFromContext(&openvr->vrContext) : nullptr; + if (!overlay) { + logger::error("DestroyOverlay: IVROverlay is nullptr"); + return; + } + if (menuOverlayHandle != vr::k_ulOverlayHandleInvalid) { + overlay->DestroyOverlay(menuOverlayHandle); + menuOverlayHandle = vr::k_ulOverlayHandleInvalid; + } + if (menuControllerOverlayHandle != vr::k_ulOverlayHandleInvalid) { + overlay->DestroyOverlay(menuControllerOverlayHandle); + menuControllerOverlayHandle = vr::k_ulOverlayHandleInvalid; + } +} + +void VR::RecreateOverlayTexturesIfNeeded() +{ + // Smart pointers automatically release existing resources when put() assigns new ones + Util::CreateOverlayTextureAndRTV(globals::d3d::device, kOverlayWidth, kOverlayHeight, menuTexture.put(), menuRTV.put()); + Util::CreateOverlayTextureAndRTV(globals::d3d::device, kOverlayWidth, kOverlayHeight, menuControllerTexture.put(), menuControllerRTV.put()); +} + +void VR::SubmitOverlayFrame() +{ + // Skip overlay operations if OpenVR is incompatible + if (!openVRInfo.isCompatible) { + return; + } + + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + auto* gameOverlay = openvr ? RE::BSOpenVR::GetIVROverlayFromContext(&openvr->vrContext) : nullptr; + auto* cleanOverlay = RE::BSOpenVR::GetCleanIVROverlay(); + + static bool cleanOverlayLogged = false; + if (!cleanOverlayLogged) { + if (cleanOverlay) { + logger::debug("VR: Successfully acquired clean IVROverlay interface via CommonLib: 0x{:X}", reinterpret_cast(cleanOverlay)); + } else { + logger::error("VR: Failed to get clean IVROverlay interface via CommonLib"); + } + cleanOverlayLogged = true; + } + + if (!gameOverlay || !cleanOverlay) { + return; + } + + if (!openvr || !openvr->vrSystem) { + logger::error("SubmitOverlayFrame: BSOpenVR or vrSystem is nullptr"); + return; + } + + // Update drag logic for all modes - only when overlay is visible + auto& enabled = globals::menu->IsEnabled; + auto& overlayVisible = globals::menu->overlayVisible; + if ((enabled || overlayVisible || settings.kAutoHideSeconds > 0) && menuOverlayHandle != vr::k_ulOverlayHandleInvalid && menuTexture.get() && menuRTV.get()) { + // Update drag logic only when overlay is active + UpdateOverlayDrag(); + // Copy ImGui output to overlay texture + ID3D11RenderTargetView* oldRTV = nullptr; + globals::d3d::context->OMGetRenderTargets(1, &oldRTV, nullptr); + ID3D11RenderTargetView* menuRTVPtr = menuRTV.get(); + globals::d3d::context->OMSetRenderTargets(1, &menuRTVPtr, nullptr); + float clearColor[4] = { 0, 0, 0, 0 }; + globals::d3d::context->ClearRenderTargetView(menuRTV.get(), clearColor); + // Re-render ImGui for HMD overlay + ImGui::Render(); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + globals::d3d::context->OMSetRenderTargets(1, &oldRTV, nullptr); + + // Apply highlight tint to HMD overlay if it's being dragged + bool hmdBeingDragged = settings.EnableDragToReposition && overlayDragState.dragging && + (overlayDragState.mode == OverlayDragState::DragMode::HMD || + overlayDragState.mode == OverlayDragState::DragMode::FixedWorld); + Util::ApplyHighlightTintToTexture(menuTexture.get(), hmdBeingDragged, settings.dragHighlightColor); + + // Update overlay position and submit to SteamVR + UpdateVROverlayPosition(); + vr::Texture_t tex = { menuTexture.get(), vr::TextureType_DirectX, vr::ColorSpace_Auto }; + if (settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) { + Util::SetOverlayInputFlags(gameOverlay, menuOverlayHandle); + vr::EVROverlayError err = cleanOverlay->SetOverlayTexture(menuOverlayHandle, &tex); + if (err != vr::VROverlayError_None) { + logger::error("SetOverlayTexture failed for menu overlay: {} ({})", static_cast(err), magic_enum::enum_name(err)); + } + err = gameOverlay->ShowOverlay(menuOverlayHandle); + if (err != vr::VROverlayError_None) { + logger::error("ShowOverlay failed for menu overlay: {} ({})", static_cast(err), magic_enum::enum_name(err)); + } + } else if (menuOverlayHandle != vr::k_ulOverlayHandleInvalid) { + gameOverlay->HideOverlay(menuOverlayHandle); + } + // Controller overlay + if (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) { + // Copy the same ImGui output to controller overlay texture + ID3D11RenderTargetView* menuControllerRTVPtr = menuControllerRTV.get(); + globals::d3d::context->OMSetRenderTargets(1, &menuControllerRTVPtr, nullptr); + globals::d3d::context->ClearRenderTargetView(menuControllerRTV.get(), clearColor); + // Re-render ImGui for controller overlay + ImGui::Render(); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + globals::d3d::context->OMSetRenderTargets(1, &oldRTV, nullptr); + + // Apply highlight tint to controller overlay if it's being dragged + bool controllerBeingDragged = overlayDragState.dragging && + overlayDragState.mode == OverlayDragState::DragMode::Controller; + Util::ApplyHighlightTintToTexture(menuControllerTexture.get(), controllerBeingDragged, settings.dragHighlightColor); + + // Position controller overlay and submit + UpdateVROverlayControllerPosition(); + + vr::Texture_t controllerTex = { menuControllerTexture.get(), vr::TextureType_DirectX, vr::ColorSpace_Auto }; + Util::SetOverlayInputFlags(gameOverlay, menuControllerOverlayHandle); + vr::EVROverlayError err = cleanOverlay->SetOverlayTexture(menuControllerOverlayHandle, &controllerTex); + if (err != vr::VROverlayError_None) { + logger::error("SetOverlayTexture failed for controller overlay: {} ({})", static_cast(err), magic_enum::enum_name(err)); + } + err = gameOverlay->ShowOverlay(menuControllerOverlayHandle); + if (err != vr::VROverlayError_None) { + logger::error("ShowOverlay failed for controller overlay: {} ({})", static_cast(err), magic_enum::enum_name(err)); + } + } else if (menuControllerOverlayHandle != vr::k_ulOverlayHandleInvalid) { + gameOverlay->HideOverlay(menuControllerOverlayHandle); + } + + // Release oldRTV after all usage is complete to prevent use-after-free + if (oldRTV) + oldRTV->Release(); + } else { + if (menuOverlayHandle != vr::k_ulOverlayHandleInvalid) { + gameOverlay->HideOverlay(menuOverlayHandle); + } + if (menuControllerOverlayHandle != vr::k_ulOverlayHandleInvalid) { + gameOverlay->HideOverlay(menuControllerOverlayHandle); + } + } +} + +// Helper to centralize VR depth buffer culling logic, reducing duplication between DataLoaded and EarlyPrepass. +void VR::UpdateDepthBufferCulling(bool desired, const FeatureConstraints::SettingId& settingId) +{ + // Check if any feature is constraining this setting + auto constraint = FeatureConstraints::GetConstraints(settingId); + + if (constraint.isConstrained) { + // Use std::get_if to safely extract bool value and avoid std::bad_variant_access + if (auto* forcedValuePtr = std::get_if(&constraint.forcedValue)) { + bool forcedValue = *forcedValuePtr; + if (gDepthBufferCulling && *gDepthBufferCulling != forcedValue) { + *gDepthBufferCulling = forcedValue; + for (const auto& src : constraint.sources) { + logger::info("{} forcing depth buffer culling {}: {}", + src.featureName, + forcedValue ? "ON" : "OFF", + src.reason); + } + } + } else { + // Constraint has non-bool value type - log warning and skip + logger::warn("VR::UpdateDepthBufferCulling: Constraint on {} has non-bool forced value, ignoring", settingId.settingPath); + } + } else { + if (gDepthBufferCulling && *gDepthBufferCulling != desired) { + *gDepthBufferCulling = desired; + logger::info("VR depth buffer culling set to {}", desired); + } + } +} + +// Handles overlay/menu open/close logic based on controller input state +void VR::UpdateOverlayMenuStateFromInput() +{ + // Disable menu interactions during combo recording + if (this->isCapturingCombo) { + return; + } + + bool& isEnabled = globals::menu->IsEnabled; + bool& overlayEnabled = globals::menu->overlayVisible; + bool& testMode = settings.VRMenuControllerDiagnosticsTestMode; + + // Auto-disable test mode if user leaves VR section or closes menu + if (testMode) { + // Check if we're still in the VR section or if menu is closed + if (!isEnabled) { + settings.VRMenuControllerDiagnosticsTestMode = false; + return; + } + // In test mode, only allow basic input processing + return; + } + + // Compute whether the game's UI menus we care about are open. Do this early so + // downstream logic can reuse the result and we only check the UI once. + bool uiMenusOpen = globals::game::ui && + (globals::game::ui->IsMenuOpen(RE::MainMenu::MENU_NAME) || globals::game::ui->IsMenuOpen(RE::TweenMenu::MENU_NAME)); + + // Valid menu state means either one of those UI menus is open, or our menu is + // enabled (but only if the game's UI system is present). + bool inValidMenuState = uiMenusOpen || (globals::game::ui && isEnabled); + + if (!inValidMenuState) + return; + + // Define menu state mappings. The `allowWhenUIMenusClosed` flag controls whether + // a mapping is allowed to run when our menu is enabled but the game's UI menus + // are not reported open. This prevents 'open' controls from firing in that state + // while still allowing 'close' actions. + struct MenuStateMapping + { + std::function condition; + std::function action; + bool allowWhenUIMenusClosed = false; + }; + + // Generic combo checking function - makes the system truly extensible + auto CheckCombo = [&](const std::vector& combos) -> bool { + if (combos.empty()) + return false; + + // Check all configured buttons in the combo + for (size_t i = 0; i < combos.size(); ++i) { + const auto& combo = combos[i]; + + bool buttonPressed = false; + + switch (combo.GetDevice()) { + case ControllerDevice::Both: + // Check if this button is pressed on BOTH controllers + buttonPressed = primaryControllerState[combo.GetKey()].isPressed && + secondaryControllerState[combo.GetKey()].isPressed; + break; + case ControllerDevice::Primary: + // Check if this button is pressed on PRIMARY controller only + buttonPressed = primaryControllerState[combo.GetKey()].isPressed; + break; + case ControllerDevice::Secondary: + // Check if this button is pressed on SECONDARY controller only + buttonPressed = secondaryControllerState[combo.GetKey()].isPressed; + break; + } + + if (!buttonPressed) { + return false; // Any button not pressed means combo fails + } + } + + // All configured buttons are pressed according to requirements + return true; + }; + + // Define the menu state mappings with extensible lambda array + std::vector mappings = { + // Open Community Shaders menu when closed + { [&]() { + return CheckCombo(settings.VRMenuOpenKeys) && !isEnabled; + }, + [&]() { isEnabled = true; } }, + + // Close Community Shaders menu when open + { [&]() { + return CheckCombo(settings.VRMenuCloseKeys) && isEnabled; + }, + [&]() { isEnabled = false; }, + true }, + + // Open VR overlay when closed + { [&]() { + return CheckCombo(settings.VROverlayOpenKeys) && !overlayEnabled; + }, + [&]() { overlayEnabled = true; } }, + + // Close VR overlay when open + { [&]() { + return CheckCombo(settings.VROverlayCloseKeys) && overlayEnabled; + }, + [&]() { overlayEnabled = false; } } + }; + + // Process mappings in order. If our menu is enabled but the game's UI menus + // are not open, only allow mappings explicitly marked with + // allowWhenUIMenusClosed (close actions). + bool onlyAllowClose = isEnabled && !uiMenusOpen; + + for (const auto& mapping : mappings) { + if (onlyAllowClose && !mapping.allowWhenUIMenusClosed) + continue; + + if (mapping.condition()) { + mapping.action(); + break; // Only execute one action per frame + } + } +} + +void VR::ProcessVREvents(std::vector& vrEvents) +{ + // Check for handedness changes and reset controller states if needed + bool currentLeftHandedMode = RE::BSOpenVRControllerDevice::IsLeftHandedMode(); + static bool firstCall = true; + if (firstCall || currentLeftHandedMode != lastKnownLeftHandedMode) { + if (!firstCall) { + logger::debug("VR handedness changed: {} -> {}", lastKnownLeftHandedMode ? "Left" : "Right", currentLeftHandedMode ? "Left" : "Right"); + } + firstCall = false; + lastKnownLeftHandedMode = currentLeftHandedMode; + // Reset controller states so they get repopulated with correct roles + primaryControllerState = {}; + secondaryControllerState = {}; + } + + double nowSecs = Util::GetNowSecs(); + for (auto& event : vrEvents) { + bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); + bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); + struct VRButtonDescriptor + { + const char* name; + bool (*isButton)(std::uint32_t); + std::uint32_t keyCode; + }; + static const VRButtonDescriptor kVRButtons[] = { + { "Grip", RE::BSOpenVRControllerDevice::IsGripButton, RE::BSOpenVRControllerDevice::Keys::kGrip }, + { "GripAlt", RE::BSOpenVRControllerDevice::IsGripButton, RE::BSOpenVRControllerDevice::Keys::kGripAlt }, + { "Trigger", RE::BSOpenVRControllerDevice::IsTriggerButton, RE::BSOpenVRControllerDevice::Keys::kTrigger }, + { "Stick Click", RE::BSOpenVRControllerDevice::IsStickClick, RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger }, + { "Touchpad Click", RE::BSOpenVRControllerDevice::IsTouchpadClick, RE::BSOpenVRControllerDevice::Keys::kTouchpadClick }, + { "Touchpad Alt", RE::BSOpenVRControllerDevice::IsTouchpadClick, RE::BSOpenVRControllerDevice::Keys::kTouchpadAlt }, + { "A/X", RE::BSOpenVRControllerDevice::IsAButton, RE::BSOpenVRControllerDevice::Keys::kXA }, + { "B/Y", RE::BSOpenVRControllerDevice::IsBButton, RE::BSOpenVRControllerDevice::Keys::kBY }, + }; + for (const auto& desc : kVRButtons) { + if (desc.isButton(event.keyCode)) { + RE::ButtonState* state = isPrimary ? &primaryControllerState[desc.keyCode] : isSecondary ? &secondaryControllerState[desc.keyCode] : + nullptr; + if (state) { + state->OnEvent(event.IsPressed(), nowSecs); + } + break; + } + } + // Do not log events here; logging is handled in the event-specific handler + switch (event.eventType) { + case RE::INPUT_EVENT_TYPE::kButton: + ProcessVRButtonEvent(event); + break; + case RE::INPUT_EVENT_TYPE::kThumbstick: + UpdateControllerState(event); + break; + default: + break; + } + } +} + +void VR::ProcessVRButtonEvent(const Menu::KeyEvent& event) +{ + // Disable menu interactions during combo recording + if (this->isCapturingCombo) { + return; + } + + ImGuiIO& io = ImGui::GetIO(); + (void)event; + bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); + bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); + bool& testMode = settings.VRMenuControllerDiagnosticsTestMode; + constexpr size_t kNumTriggerMappings = 1; + + // Process button mappings for the current controller + if (isPrimary || isSecondary) { + // Define mappings for both controllers (only B/Y differs) + constexpr size_t kNumMappings = 6; + RE::ButtonMapping mappings[kNumMappings] = { + { RE::BSOpenVRControllerDevice::Keys::kTrigger, ImGuiMouseButton_Left, false, ImGuiKey_None, false }, + { RE::BSOpenVRControllerDevice::Keys::kGrip, ImGuiMouseButton_Right, false, ImGuiKey_None, false }, + { RE::BSOpenVRControllerDevice::Keys::kTouchpadClick, ImGuiMouseButton_Middle, false, ImGuiKey_None, false }, + { RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger, ImGuiMouseButton_Middle, false, ImGuiKey_None, false }, + { RE::BSOpenVRControllerDevice::Keys::kBY, -1, true, Util::Input::VirtualKeyToImGuiKey(VK_TAB), isSecondary }, // Shift+Tab for secondary + { RE::BSOpenVRControllerDevice::Keys::kXA, -1, true, Util::Input::VirtualKeyToImGuiKey(VK_RETURN), false }, + }; + + // Use separate state arrays for each controller + static bool prevPrimaryStates[kNumMappings] = {}; + static bool prevSecondaryStates[kNumMappings] = {}; + bool* prevStates = isPrimary ? prevPrimaryStates : prevSecondaryStates; + + // Get the appropriate controller state + RE::InputDeviceState& controllerState = isPrimary ? primaryControllerState : secondaryControllerState; + + size_t limit = testMode ? kNumTriggerMappings : kNumMappings; // Only trigger mappings in test mode + + for (size_t i = 0; i < limit; ++i) { + RE::ButtonState* state = &controllerState[mappings[i].keyCode]; + bool curr = state ? state->isPressed : false; + if (curr != prevStates[i]) { + if (mappings[i].isKeyEvent) { + if (mappings[i].isShift) + io.AddKeyEvent(ImGuiMod_Shift, curr); + io.AddKeyEvent(static_cast(mappings[i].key), curr); + } else { + io.AddMouseButtonEvent(mappings[i].logicalButton, curr); + } + prevStates[i] = curr; + } + } + } + // Log the button event after state is updated + VRControllerEventLog logEntry; + logEntry.device = static_cast(event.device); + logEntry.keyCode = event.keyCode; + logEntry.value = static_cast(event.value); + logEntry.pressed = event.IsPressed(); + logEntry.heldTime = 0.0; + logEntry.heldSource = "button"; + logEntry.thumbstickX = 0.0f; + logEntry.thumbstickY = 0.0f; + logEntry.controllerRole = isPrimary ? "Primary" : isSecondary ? "Secondary" : + "Unknown"; + vrControllerEventLog.push_back(logEntry); + if (vrControllerEventLog.size() > 32) { + vrControllerEventLog.erase(vrControllerEventLog.begin()); + } +} + +void VR::UpdateControllerState(const Menu::KeyEvent& event) +{ + bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); + bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); + + // Update thumbstick state for diagnostics display and later input processing + if (isPrimary) { + primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x = event.thumbstickX; + primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y = event.thumbstickY; + } else if (isSecondary) { + secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x = event.thumbstickX; + secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y = event.thumbstickY; + } + + // Log the thumbstick event + VRControllerEventLog logEntry; + logEntry.device = static_cast(event.device); + logEntry.keyCode = event.keyCode; + logEntry.value = static_cast(event.value); + logEntry.pressed = event.IsPressed(); + logEntry.heldTime = 0.0; + logEntry.heldSource = "thumbstick"; + logEntry.thumbstickX = event.thumbstickX; + logEntry.thumbstickY = event.thumbstickY; + logEntry.controllerRole = isPrimary ? "Primary" : "Secondary"; + vrControllerEventLog.push_back(logEntry); + if (vrControllerEventLog.size() > 32) { + vrControllerEventLog.erase(vrControllerEventLog.begin()); + } +} + +// Helper function to process thumbstick input for scrolling +void VR::ProcessThumbstickScroll(RE::VRControllerState& controllerState, size_t thumbstickIndex, float deadzone, ImGuiIO& io) +{ + bool usingScrollStickX = (std::abs(controllerState.thumbsticks[thumbstickIndex].x) > deadzone); + bool usingScrollStickY = (std::abs(controllerState.thumbsticks[thumbstickIndex].y) > deadzone); + + if (usingScrollStickX || usingScrollStickY) { + // Per-controller scroll accumulation to prevent interference between controllers + struct ScrollAccum + { + float x = 0.0f; + float y = 0.0f; + }; + static std::unordered_map scrollAccums; + ScrollAccum& accum = scrollAccums[thumbstickIndex]; + + // Accumulate scroll input with sensitivity scaling + accum.x += controllerState.thumbsticks[thumbstickIndex].x * 0.1f; + accum.y += controllerState.thumbsticks[thumbstickIndex].y * 0.1f; + + // Send scroll events when accumulated enough input + float scrollEventX = 0.0f; + float scrollEventY = 0.0f; + + if (std::abs(accum.x) > 0.3f) { + scrollEventX = accum.x > 0 ? 1.0f : -1.0f; + accum.x = 0.0f; + } + if (std::abs(accum.y) > 0.3f) { + scrollEventY = accum.y > 0 ? 1.0f : -1.0f; + accum.y = 0.0f; + } + + // Send both horizontal and vertical scroll events if needed + if (scrollEventX != 0.0f || scrollEventY != 0.0f) { + io.AddMouseWheelEvent(scrollEventX, scrollEventY); + } + } +} + +// Converts VR controller input to ImGui mouse and scroll events for the overlay UI +// Supports both modern wand pointing and traditional thumbstick control +void VR::ProcessControllerInputForImGui() +{ + if (!globals::menu->IsEnabled) + return; + bool testMode = settings.VRMenuControllerDiagnosticsTestMode; + float mouseDeadzone = settings.mouseDeadzone; + float mouseSpeed = settings.mouseSpeed; + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; + io.WantSetMousePos = false; + + // Try wand pointing first + bool wandHandledCursor = false; + if (!testMode && settings.EnableWandPointing) { + UpdateCursorFromWandPointing(); + wandHandledCursor = wandState.isIntersecting; + } + + if (!testMode) { + // When wand is handling cursor, BOTH thumbsticks become scroll controls + // When wand is NOT active, use traditional cursor/scroll assignment + if (wandHandledCursor) { + // Wand mode: Both thumbsticks scroll + ProcessThumbstickScroll(primaryControllerState, static_cast(RE::ControllerRole::Primary), mouseDeadzone, io); + ProcessThumbstickScroll(secondaryControllerState, static_cast(RE::ControllerRole::Secondary), mouseDeadzone, io); + } else { + // Traditional mode: Determine which controller handles cursor vs scrolling + bool useAttachedControllerForCursor = (settings.attachMode == VR::Settings::OverlayAttachMode::ControllerOnly || + settings.attachMode == VR::Settings::OverlayAttachMode::Both); + + RE::VRControllerState* cursorController = nullptr; + RE::VRControllerState* scrollController = nullptr; + + if (useAttachedControllerForCursor) { + // When attached to controller: attached controller = cursor, other controller = scrolling + if (settings.VRMenuAttachController == ControllerDevice::Primary) { + cursorController = &primaryControllerState; + scrollController = &secondaryControllerState; + } else { + cursorController = &secondaryControllerState; + scrollController = &primaryControllerState; + } + } else { + // HMD mode: primary = cursor, secondary = scroll (traditional) + cursorController = &primaryControllerState; + scrollController = &secondaryControllerState; + } + + // Cursor movement (from determined cursor controller) + if (cursorController) { + size_t thumbstickIndex = (cursorController == &primaryControllerState) ? + static_cast(RE::ControllerRole::Primary) : + static_cast(RE::ControllerRole::Secondary); + + float thumbstickX = cursorController->thumbsticks[thumbstickIndex].x; + float thumbstickY = cursorController->thumbsticks[thumbstickIndex].y; + bool usingCursorStick = (std::abs(thumbstickX) > mouseDeadzone || std::abs(thumbstickY) > mouseDeadzone); + + if (usingCursorStick) { + ImVec2 mousePos = io.MousePos; + mousePos.x += thumbstickX * mouseSpeed; + mousePos.y -= thumbstickY * mouseSpeed; + mousePos.x = std::clamp(mousePos.x, 0.0f, io.DisplaySize.x); + mousePos.y = std::clamp(mousePos.y, 0.0f, io.DisplaySize.y); + io.MousePos = mousePos; + io.AddMousePosEvent(mousePos.x, mousePos.y); + io.MouseDrawCursor = true; + io.WantSetMousePos = true; + io.AddMouseButtonEvent(ImGuiMouseButton_Left, false); + } + } + + // Scrolling (from determined scroll controller) + if (scrollController) { + size_t thumbstickIndex = (scrollController == &primaryControllerState) ? + static_cast(RE::ControllerRole::Primary) : + static_cast(RE::ControllerRole::Secondary); + ProcessThumbstickScroll(*scrollController, thumbstickIndex, mouseDeadzone, io); + } + } + } +} + +// --- File-scope static helpers for drag logic --- +static bool CanStartAny(vr::ETrackedControllerRole role) +{ + return role != vr::TrackedControllerRole_Invalid; +} + +void VR::UpdateOverlayDrag() +{ + if (!CanPerformDrag()) { + return; + } + + if (overlayDragState.dragging) { + UpdateActiveDrag(); + } else { + TryStartNewDrag(); + } +} + +bool VR::CanPerformDrag() +{ + if (!settings.EnableDragToReposition) + return false; + + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + auto* system = openvr ? openvr->vrSystem : nullptr; + if (!system) + return false; + + // Check if test mode is active - disable all dragging + if (settings.VRMenuControllerDiagnosticsTestMode) { + return false; + } + + return true; +} + +void VR::UpdateActiveDrag() +{ + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + auto* system = openvr ? openvr->vrSystem : nullptr; + if (!system) + return; + + // Helper to get grip state for a controller based on actual hand position + auto getGripPressed = [&](bool isLeft, bool isRight) { + bool isLeftHanded = lastKnownLeftHandedMode; + + if (isLeft) { + // Left hand: primary if left-handed, secondary if right-handed + if (isLeftHanded) { + return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } else { + return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } + } + if (isRight) { + // Right hand: secondary if left-handed, primary if right-handed + if (isLeftHanded) { + return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } else { + return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } + } + return false; + }; + + // Helper to reset drag state + auto resetDragState = [&]() { + overlayDragState.dragging = false; + overlayDragState.controllerIndex = vr::k_unTrackedDeviceIndexInvalid; + overlayDragState.isPrimary = false; + overlayDragState.isSecondary = false; + }; + + float rawMatrix[3][4]; + if (Util::GetControllerWorldMatrix(overlayDragState.controllerIndex, rawMatrix)) { + vr::HmdMatrix34_t mat = Util::Float3x4ToHmdMatrix34(rawMatrix); + Matrix controllerMatrix = Util::HmdMatrix34ToMatrix(mat); + + // Update drag based on current mode + switch (overlayDragState.mode) { + case OverlayDragState::DragMode::Controller: + { + // Get current attached controller transform to convert world deltas to local space + vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + + if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { + vr::TrackedDevicePose_t controllerPose; + if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &controllerPose, 1)) + break; + if (controllerPose.bPoseIsValid) { + Matrix attachedControllerMatrix = Util::HmdMatrix34ToMatrix(controllerPose.mDeviceToAbsoluteTracking); + + // Calculate world-space delta + Vector3 worldDelta( + controllerMatrix._14 - overlayDragState.initialControllerMatrix._14, + controllerMatrix._24 - overlayDragState.initialControllerMatrix._24, + controllerMatrix._34 - overlayDragState.initialControllerMatrix._34); + + // Transform world delta to attached controller local space (use transpose for correct direction) + Vector3 localDelta = Vector3::Transform(worldDelta, attachedControllerMatrix); + + // Apply local delta to offsets + settings.VRMenuControllerOffsetX = overlayDragState.initialControllerOffset.x + localDelta.x; + settings.VRMenuControllerOffsetY = overlayDragState.initialControllerOffset.y + localDelta.y; + settings.VRMenuControllerOffsetZ = overlayDragState.initialControllerOffset.z + localDelta.z; + UpdateVROverlayPosition(); + } + } + break; + } + case OverlayDragState::DragMode::FixedWorld: + { + Matrix delta = controllerMatrix * overlayDragState.initialControllerMatrix.Invert(); + fixedWorldOverlayPosition.m = delta * overlayDragState.initialOverlayMatrix; + break; + } + case OverlayDragState::DragMode::HMD: + { + // Get current HMD transform to convert world deltas to local space + vr::TrackedDevicePose_t hmdPose; + if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &hmdPose, 1)) + break; + if (hmdPose.bPoseIsValid) { + Matrix hmdMatrix = Util::HmdMatrix34ToMatrix(hmdPose.mDeviceToAbsoluteTracking); + + // Calculate world-space delta + Vector3 worldDelta( + controllerMatrix._14 - overlayDragState.initialControllerMatrix._14, + controllerMatrix._24 - overlayDragState.initialControllerMatrix._24, + controllerMatrix._34 - overlayDragState.initialControllerMatrix._34); + + // Transform world delta to HMD local space (use transpose for correct direction) + Vector3 localDelta = Vector3::Transform(worldDelta, hmdMatrix); + + // Apply local delta to offsets + settings.VRMenuOffsetX = overlayDragState.initialHMDOffset.x + localDelta.x; + settings.VRMenuOffsetY = overlayDragState.initialHMDOffset.y + localDelta.y; + settings.VRMenuOffsetZ = overlayDragState.initialHMDOffset.z + localDelta.z; + UpdateVROverlayPosition(); + } + break; + } + } + } + + bool gripPressed = getGripPressed(overlayDragState.isPrimary, overlayDragState.isSecondary); + if (!gripPressed) { + resetDragState(); + } +} + +void VR::TryStartNewDrag() +{ + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + auto* system = openvr ? openvr->vrSystem : nullptr; + if (!system) + return; + + // Helper to get grip state for a controller based on actual hand position + auto getGripPressed = [&](bool isLeft, bool isRight) { + bool isLeftHanded = lastKnownLeftHandedMode; + + if (isLeft) { + // Left hand: primary if left-handed, secondary if right-handed + if (isLeftHanded) { + return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } else { + return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } + } + if (isRight) { + // Right hand: secondary if left-handed, primary if right-handed + if (isLeftHanded) { + return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } else { + return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } + } + return false; + }; + + // --- Strict mutually exclusive drag mode selection --- + struct DragMode + { + OverlayDragState::DragMode mode; + bool isActive; + std::function canStart; + std::function onInit; + }; + + std::vector dragModes; + + // Controller mode - only for opposite hand (highest priority) + if (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) { + // Controller drag - only opposite hand can drag the controller overlay + dragModes.push_back({ OverlayDragState::DragMode::Controller, + true, + [&](vr::ETrackedControllerRole role) { + // Get the attached controller index + vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + if (attachedControllerIndex == vr::k_unTrackedDeviceIndexInvalid) { + return false; // No attached controller found + } + + // Get the opposite controller index + ControllerDevice oppositeDevice = (settings.VRMenuAttachController == ControllerDevice::Primary) ? + ControllerDevice::Secondary : + ControllerDevice::Primary; + vr::TrackedDeviceIndex_t oppositeControllerIndex = Util::GetControllerIndexForDevice(oppositeDevice, lastKnownLeftHandedMode); + if (oppositeControllerIndex == vr::k_unTrackedDeviceIndexInvalid) { + return false; // No opposite controller found + } + + // Check if the current controller (the one doing the dragging) is the opposite controller + for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { + if (system->GetTrackedDeviceClass(i) == vr::TrackedDeviceClass_Controller) { + vr::ETrackedControllerRole deviceRole = system->GetControllerRoleForTrackedDeviceIndex(i); + if (deviceRole == role && i == oppositeControllerIndex) { + return true; // This is the opposite controller, can drag + } + } + } + return false; // Not the opposite controller + }, + [&]() { + overlayDragState.initialControllerOffset.x = settings.VRMenuControllerOffsetX; + overlayDragState.initialControllerOffset.y = settings.VRMenuControllerOffsetY; + overlayDragState.initialControllerOffset.z = settings.VRMenuControllerOffsetZ; + overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; + } }); + } + + // Fixed world mode - only for attached controller when in "Both" mode + if (settings.VRMenuPositioningMethod == 1) { + // In "Both" mode, only the attached controller can adjust fixed world position + // In HMD-only mode, any controller can adjust fixed world position + std::function fixedWorldCanStart; + if (settings.attachMode == AttachMode::Both) { + // In "Both" mode, only the attached controller can adjust fixed world position + fixedWorldCanStart = [&](vr::ETrackedControllerRole role) { + // Find the actual attached controller using helper function + vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + + if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { + vr::ETrackedControllerRole actualAttachedRole = system->GetControllerRoleForTrackedDeviceIndex(attachedControllerIndex); + // Only allow the attached controller to drag fixed world + return role == actualAttachedRole; + } + return false; + }; + } else { + // In HMD-only mode, any controller can adjust fixed world position + fixedWorldCanStart = CanStartAny; + } + + dragModes.push_back({ OverlayDragState::DragMode::FixedWorld, + true, + fixedWorldCanStart, + [&]() { + overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; + overlayDragState.initialOverlayMatrix = fixedWorldOverlayPosition.m; + } }); + } + + // HMD mode - for attached controller when both modes active, or any controller otherwise + if (settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) { + // In "Both" mode, only the attached controller can adjust HMD position + // In HMD-only mode, any controller can adjust HMD position + std::function hmdCanStart; + if (settings.attachMode == AttachMode::Both) { + // In "Both" mode, only the attached controller can adjust HMD position + hmdCanStart = [&](vr::ETrackedControllerRole role) { + // Find the actual attached controller using helper function + vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + + if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { + vr::ETrackedControllerRole actualAttachedRole = system->GetControllerRoleForTrackedDeviceIndex(attachedControllerIndex); + // Only allow the attached controller to drag HMD + return role == actualAttachedRole; + } + return false; + }; + } else { + // In HMD-only mode, any controller can adjust HMD + hmdCanStart = CanStartAny; + } + + dragModes.push_back({ OverlayDragState::DragMode::HMD, + true, + hmdCanStart, + [&]() { + overlayDragState.initialHMDOffset.x = settings.VRMenuOffsetX; + overlayDragState.initialHMDOffset.y = settings.VRMenuOffsetY; + overlayDragState.initialHMDOffset.z = settings.VRMenuOffsetZ; + overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; + } }); + } + + // Try to start a new drag - use first available mode + for (const auto& mode : dragModes) { + if (!mode.isActive) + continue; + for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { + if (system->GetTrackedDeviceClass(i) != vr::TrackedDeviceClass_Controller) + continue; + vr::ETrackedControllerRole role = system->GetControllerRoleForTrackedDeviceIndex(i); + bool isLeft = (role == vr::ETrackedControllerRole::TrackedControllerRole_LeftHand); + bool isRight = (role == vr::ETrackedControllerRole::TrackedControllerRole_RightHand); + if (!mode.canStart(role)) + continue; + bool gripPressed = getGripPressed(isLeft, isRight); + if (!gripPressed) + continue; + float rawMatrix[3][4]; + if (!Util::GetControllerWorldMatrix(i, rawMatrix)) + continue; + vr::HmdMatrix34_t mat = Util::Float3x4ToHmdMatrix34(rawMatrix); + Matrix controllerMatrix = Util::HmdMatrix34ToMatrix(mat); + overlayDragState.dragging = true; + overlayDragState.mode = mode.mode; + overlayDragState.controllerIndex = i; + overlayDragState.isPrimary = isLeft; + overlayDragState.isSecondary = isRight; + overlayDragState.startControllerMatrix = controllerMatrix; + mode.onInit(); + + // Send haptic pulse to the controller that started the drag (only if overlay is visible) + if (system && globals::menu->IsEnabled) { + // Find the controller device index for the hand that started the drag + for (vr::TrackedDeviceIndex_t deviceIdx = 0; deviceIdx < vr::k_unMaxTrackedDeviceCount; ++deviceIdx) { + if (system->GetTrackedDeviceClass(deviceIdx) == vr::TrackedDeviceClass_Controller) { + vr::ETrackedControllerRole deviceRole = system->GetControllerRoleForTrackedDeviceIndex(deviceIdx); + bool isRightController = (deviceRole == vr::ETrackedControllerRole::TrackedControllerRole_RightHand); + if (isRightController == isRight) { + // Use BSOpenVR's haptic pulse method instead of direct OpenVR call + openvr->TriggerHapticPulse(isRightController, 25.0f); // 100ms pulse + break; + } + } + } + } + + return; + } + } +} + +void VR::SetFixedOverlayToCurrentHMD() +{ + vr::HmdMatrix34_t transform = Util::ComputeOverlayTransformFromHMD( + settings.VRMenuOffsetX, + settings.VRMenuOffsetY, + settings.VRMenuOffsetZ); + fixedWorldOverlayPosition.m = Util::HmdMatrix34ToMatrix(transform); +} + +//============================================================================= +// OPENVR VERSION DETECTION AND COMPATIBILITY +//============================================================================= + +void VR::DetectOpenVRInfo() +{ + // Reset info + openVRInfo = {}; + + // Find the OpenVR DLL module + HMODULE hModule = GetModuleHandleA("openvr_api.dll"); + if (!hModule) { + openVRInfo.isAvailable = false; + return; + } + + openVRInfo.isAvailable = true; + + // Get the full path to the DLL + char dllPath[MAX_PATH]; + if (GetModuleFileNameA(hModule, dllPath, MAX_PATH) == 0) { + openVRInfo.isCompatible = false; + return; + } + + openVRInfo.dllPath = dllPath; + + // Get file version information + DWORD dwSize = GetFileVersionInfoSizeA(dllPath, nullptr); + if (dwSize > 0) { + std::vector buffer(dwSize); + if (GetFileVersionInfoA(dllPath, 0, dwSize, buffer.data())) { + VS_FIXEDFILEINFO* pFileInfo = nullptr; + UINT len = 0; + if (VerQueryValueA(buffer.data(), "\\", (LPVOID*)&pFileInfo, &len)) { + DWORD major = HIWORD(pFileInfo->dwFileVersionMS); + DWORD minor = LOWORD(pFileInfo->dwFileVersionMS); + DWORD build = HIWORD(pFileInfo->dwFileVersionLS); + DWORD revision = LOWORD(pFileInfo->dwFileVersionLS); + openVRInfo.version = std::format("{}.{}.{}.{}", major, minor, build, revision); + } + } + } + + if (openVRInfo.version.empty()) { + openVRInfo.version = "Unknown"; + } + + // Get file size and timestamp + WIN32_FIND_DATAA findData; + HANDLE hFind = FindFirstFileA(dllPath, &findData); + if (hFind != INVALID_HANDLE_VALUE) { + FindClose(hFind); + ULARGE_INTEGER fileSize; + fileSize.LowPart = findData.nFileSizeLow; + fileSize.HighPart = findData.nFileSizeHigh; + openVRInfo.fileSize = fileSize.QuadPart; + + // Convert file time to readable format + SYSTEMTIME st; + FileTimeToSystemTime(&findData.ftLastWriteTime, &st); + openVRInfo.modificationTime = std::format("{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}", + st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); + } + + // Check compatibility + openVRInfo.isCompatible = IsOpenVRCompatible(); +} + +bool VR::IsOpenVRCompatible() const +{ + if (!openVRInfo.isAvailable) { + return false; + } + + // Whitelist: Only allow explicitly known compatible versions + struct WhitelistedVersion + { + std::string version; + uint64_t fileSize; + std::string modificationTime; + }; + + static const std::vector whitelist = { + { "1.0.10.0", 598816, "2022-04-18 00:47:59" }, + // Add more known compatible versions here + }; + + for (const auto& entry : whitelist) { + if (openVRInfo.version == entry.version) { + return true; + } + } + + return false; // Not compatible unless explicitly whitelisted +} + +//============================================================================= +// WAND POINTING IMPLEMENTATION +//============================================================================= + +bool VR::ComputeWandIntersection(vr::VROverlayHandle_t overlayHandle, vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV) +{ + Util::OpenVRContext ctx; + if (!ctx.HasOverlay()) + return false; + + // Use utility function for core intersection logic + bool intersected = Util::ComputeWandIntersection(ctx.overlay, overlayHandle, controllerIndex, outUV); + + if (intersected) { + // Update wand state for debugging and visualization + wandState.isIntersecting = true; + wandState.uvCoordinates = outUV; + wandState.controllerIndex = controllerIndex; + + // Get controller pose for ray geometry (for debugging/laser visualization) + vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount]; + if (Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, poses, vr::k_unMaxTrackedDeviceCount)) { + if (poses[controllerIndex].bPoseIsValid) { + wandState.rayOrigin = Vector3( + poses[controllerIndex].mDeviceToAbsoluteTracking.m[0][3], + poses[controllerIndex].mDeviceToAbsoluteTracking.m[1][3], + poses[controllerIndex].mDeviceToAbsoluteTracking.m[2][3]); + wandState.rayDirection = Vector3( + -poses[controllerIndex].mDeviceToAbsoluteTracking.m[0][2], + -poses[controllerIndex].mDeviceToAbsoluteTracking.m[1][2], + -poses[controllerIndex].mDeviceToAbsoluteTracking.m[2][2]); + } + } + } else { + wandState.isIntersecting = false; + } + + return intersected; +} + +void VR::UpdateCursorFromWandPointing() +{ + // Only update cursor when menu is active (ImGui must be initialized) + if (!settings.EnableWandPointing || !globals::menu->IsEnabled) + return; + + ImGuiIO& io = ImGui::GetIO(); + + // Determine which controller should be used for pointing + // Use non-attached controller (free hand points at menu on other hand) + vr::TrackedDeviceIndex_t pointingController = vr::k_unTrackedDeviceIndexInvalid; + + if (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) { + // If menu is attached to a controller, use the OTHER controller for pointing + ControllerDevice oppositeController = (settings.VRMenuAttachController == ControllerDevice::Primary) ? + ControllerDevice::Secondary : + ControllerDevice::Primary; + pointingController = Util::GetControllerIndexForDevice(oppositeController, lastKnownLeftHandedMode); + } else { + // HMD-only mode: use primary (dominant) hand for pointing + pointingController = Util::GetControllerIndexForDevice(ControllerDevice::Primary, lastKnownLeftHandedMode); + } + + if (pointingController == vr::k_unTrackedDeviceIndexInvalid) { + wandState.isIntersecting = false; + return; + } + + // Try to get intersection with active overlay + ImVec2 uv; + bool intersected = false; + + // Check HMD overlay first + if (menuOverlayHandle != vr::k_ulOverlayHandleInvalid) { + if (ComputeWandIntersection(menuOverlayHandle, pointingController, uv)) { + intersected = true; + } + } + + // If HMD overlay didn't intersect, try controller overlay + if (!intersected && menuControllerOverlayHandle != vr::k_ulOverlayHandleInvalid) { + if (ComputeWandIntersection(menuControllerOverlayHandle, pointingController, uv)) { + intersected = true; + } + } + + // Update cursor position from intersection + if (intersected) { + // Convert UV (0-1) to screen pixel coordinates + // Invert Y to match laser visualization (OpenVR UV has 0 at bottom) + float screenX = uv.x * io.DisplaySize.x; + float screenY = (1.0f - uv.y) * io.DisplaySize.y; + + // Clamp to screen bounds (allow full display size, consistent with thumbstick cursor) + screenX = std::clamp(screenX, 0.0f, io.DisplaySize.x); + screenY = std::clamp(screenY, 0.0f, io.DisplaySize.y); + + io.MousePos = ImVec2(screenX, screenY); + io.AddMousePosEvent(screenX, screenY); + io.MouseDrawCursor = true; + io.WantSetMousePos = true; + } else { + // Ensure wand state is cleared if no intersection + wandState.isIntersecting = false; + } } diff --git a/src/Features/VR.h b/src/Features/VR.h index 7fbfcf2f68..67f02de31d 100644 --- a/src/Features/VR.h +++ b/src/Features/VR.h @@ -3,7 +3,6 @@ #include "Menu.h" #include "OverlayFeature.h" #include "Utils/Input.h" -#include "VR/OpenVRDetection.h" // In Features/VR/ #include #include #include @@ -61,14 +60,9 @@ struct VR : OverlayFeature */ struct Config { - // Overlay texture dimensions - static constexpr int kOverlayWidth = 1920; ///< Overlay texture width in pixels - static constexpr int kOverlayHeight = 1080; ///< Overlay texture height in pixels - static constexpr float kOverlayAspect = static_cast(kOverlayHeight) / static_cast(kOverlayWidth); ///< Aspect ratio (height/width) - static constexpr float kDefaultMenuScale = 1.0f; ///< Default overlay scale factor - static constexpr float kMinMenuScale = 0.1f; ///< Minimum allowed overlay scale - static constexpr float kMaxMenuScale = 5.0f; ///< Maximum allowed overlay scale + static constexpr float kMinMenuScale = 0.5f; ///< Minimum allowed overlay scale + static constexpr float kMaxMenuScale = 2.0f; ///< Maximum allowed overlay scale static constexpr float kDefaultComboTimeout = 3.0f; ///< Default timeout for button combos (seconds) static constexpr float kDefaultMouseDeadzone = 0.1f; ///< Default thumbstick deadzone for mouse input static constexpr float kDefaultMouseSpeed = 10.0f; ///< Default mouse speed multiplier @@ -76,14 +70,14 @@ struct VR : OverlayFeature static constexpr int kMaxAutoHideSeconds = 300; ///< Maximum auto-hide timeout (5 minutes) // Default HMD overlay offset values (in meters, relative to HMD) - static constexpr float kDefaultHMDOffsetX = 0.195f; ///< Default horizontal offset from HMD - static constexpr float kDefaultHMDOffsetY = -0.375f; ///< Default vertical offset from HMD - static constexpr float kDefaultHMDOffsetZ = -1.355f; ///< Default depth offset from HMD + static constexpr float kDefaultHMDOffsetX = 0.26f; ///< Default horizontal offset from HMD + static constexpr float kDefaultHMDOffsetY = -0.04f; ///< Default vertical offset from HMD + static constexpr float kDefaultHMDOffsetZ = -0.41f; ///< Default depth offset from HMD // Default controller overlay offset values (in meters, relative to controller) - static constexpr float kDefaultControllerOffsetX = 0.295f; ///< Default horizontal offset from controller - static constexpr float kDefaultControllerOffsetY = 0.211f; ///< Default vertical offset from controller - static constexpr float kDefaultControllerOffsetZ = 0.063f; ///< Default depth offset from controller + static constexpr float kDefaultControllerOffsetX = 0.22f; ///< Default horizontal offset from controller + static constexpr float kDefaultControllerOffsetY = 0.15f; ///< Default vertical offset from controller + static constexpr float kDefaultControllerOffsetZ = 0.20f; ///< Default depth offset from controller }; //============================================================================= @@ -97,11 +91,10 @@ struct VR : OverlayFeature return { "Provides VR-specific optimizations and enhancements for Community Shaders, improving performance and visual quality in virtual reality environments.", { "Depth buffer culling optimization for VR performance", - "In-scene overlay menu with HMD/Controller/Fixed World attach modes", - "VR controller input with customizable button mappings", - "Grip-to-drag overlay positioning with depth control", "Configurable occlusion culling parameters", - "Enhanced VR compatibility with SteamVR and OpenComposite" } + "VR-specific rendering pipeline improvements", + "Performance optimizations for dual-eye rendering", + "Enhanced VR compatibility across all shader features" } }; } @@ -128,7 +121,7 @@ struct VR : OverlayFeature //============================================================================= virtual void DrawOverlay() override; - virtual bool IsOverlayVisible() const override { return IsOpenVRCompatible() && settings.kAutoHideSeconds > 0 && globals::menu && !globals::menu->IsEnabled; } + virtual bool IsOverlayVisible() const override { return openVRInfo.isCompatible && settings.kAutoHideSeconds > 0 && !globals::menu->IsEnabled; } //============================================================================= // SETTINGS STRUCTURE @@ -160,8 +153,7 @@ struct VR : OverlayFeature { HMDOnly = 0, ///< Overlay attached to HMD only ControllerOnly = 1, ///< Overlay attached to controller only - Both = 2, ///< Overlay can be attached to both HMD and controller - None = 3 ///< Overlay display disabled + Both = 2 ///< Overlay can be attached to both HMD and controller }; OverlayAttachMode attachMode = OverlayAttachMode::HMDOnly; ///< Current overlay attachment mode ControllerDevice VRMenuAttachController = ControllerDevice::Secondary; ///< Which controller to attach overlay to @@ -224,7 +216,7 @@ struct VR : OverlayFeature */ bool IsAttachModeValid() const { - return attachMode >= OverlayAttachMode::HMDOnly && attachMode <= OverlayAttachMode::None; + return attachMode >= OverlayAttachMode::HMDOnly && attachMode <= OverlayAttachMode::Both; } /** @@ -250,16 +242,13 @@ struct VR : OverlayFeature // VR-SPECIFIC PUBLIC API //============================================================================= + void UpdateVROverlayPosition(); + void UpdateVROverlayControllerPosition(); + void ProcessVREvents(std::vector& vrEvents); // Wand pointing methods - enum class OverlayType - { - HMD, - Controller - }; - bool ComputeWandIntersection(vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV); - bool ComputeWandIntersectionForOverlayType(OverlayType type, vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV); + bool ComputeWandIntersection(vr::VROverlayHandle_t overlayHandle, vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV); void UpdateCursorFromWandPointing(); void UpdateOverlayMenuStateFromInput(); void ProcessVRButtonEvent(const Menu::KeyEvent& event); @@ -267,6 +256,8 @@ struct VR : OverlayFeature void ProcessThumbstickScroll(RE::VRControllerState& controllerState, size_t thumbstickIndex, float deadzone, ImGuiIO& io); void ProcessControllerInputForImGui(); + void EnsureOverlayInitialized(); + void DestroyOverlay(); void RecreateOverlayTexturesIfNeeded(); void SubmitOverlayFrame(); @@ -318,7 +309,6 @@ struct VR : OverlayFeature void UpdateActiveDrag(); void TryStartNewDrag(); void SetFixedOverlayToCurrentHMD(); - void UpdateFixedWorldPositioning(); bool ShouldHighlightOverlayWindow() const { return overlayDragState.dragging; } //============================================================================= @@ -359,7 +349,6 @@ struct VR : OverlayFeature struct OverlayWorldPosition { Matrix m = Matrix::Identity; - bool initialized = false; } fixedWorldOverlayPosition; struct OverlayDragState @@ -383,7 +372,6 @@ struct VR : OverlayFeature Vector3 initialHMDOffset = Vector3::Zero; Vector3 initialControllerOffset = Vector3::Zero; - float initialHMDScale = 1.0f; Matrix startControllerMatrix = Matrix::Identity; } overlayDragState; @@ -425,15 +413,6 @@ struct VR : OverlayFeature std::string version; uint64_t fileSize = 0; std::string modificationTime; - - // Interface probing results - bool hasOverlayInterface = false; - bool hasSystemInterface = false; - bool hasCompositorInterface = false; - - // Detection metadata - VRDetection::RuntimeType runtimeType = VRDetection::RuntimeType::Unknown; - bool probingSucceeded = false; } openVRInfo; RE::NiPoint3 savedPlayerWorldPos = RE::NiPoint3(); // Used for auto-reset distance check @@ -448,44 +427,11 @@ struct VR : OverlayFeature Vector3 rayDirection = Vector3::Zero; } wandState; - // In-Scene Overlay Rendering Resources (Fallback for incompatible runtimes) - struct InSceneResources - { - winrt::com_ptr vs; - winrt::com_ptr ps; - winrt::com_ptr vb; - winrt::com_ptr ib; - winrt::com_ptr cb; - winrt::com_ptr inputLayout; - winrt::com_ptr blendState; - winrt::com_ptr depthState; - winrt::com_ptr sampler; - winrt::com_ptr rasterizerState; - - // Cached SRV to avoid creating every frame - winrt::com_ptr menuSRV; - ID3D11Texture2D* cachedMenuTexture = nullptr; - - bool initialized = false; - } inSceneResources; - - struct InSceneCB - { - Matrix wvp; - }; - - void InitInSceneResources(); - void RenderInSceneOverlay(vr::EVREye eye, ID3D11Texture2D* targetTexture, const vr::VRTextureBounds_t* bounds); - void InstallSubmitHook(); - void DetectOpenVRInfo(); - bool IsOpenVRCompatible() const; - -private: +public: //============================================================================= - // PRIVATE HELPERS + // PRIVATE IMPLEMENTATION //============================================================================= - bool GetGripPressed(bool isLeft, bool isRight) const; - void ResetComboRecording(); - void ApplyRecordedCombo(); + void DetectOpenVRInfo(); + bool IsOpenVRCompatible() const; }; diff --git a/src/Features/VR/InSceneOverlay.cpp b/src/Features/VR/InSceneOverlay.cpp deleted file mode 100644 index f079659966..0000000000 --- a/src/Features/VR/InSceneOverlay.cpp +++ /dev/null @@ -1,611 +0,0 @@ -#include "Features/VR.h" -#include "Globals.h" -#include "Hooks.h" -#include "Menu.h" -#include "Util.h" -#include "Utils/VRUtils.h" -#include -#include -#include -#include -#include -#include - -using namespace DirectX; -using namespace DirectX::SimpleMath; - -using AttachMode = VR::Settings::OverlayAttachMode; - -// Helper to create aspect-corrected scale matrix for overlay -inline Matrix CreateOverlayScaleMatrix(float scale) -{ - return Matrix::CreateScale(scale, scale * VR::Config::kOverlayAspect, scale); -} - -//============================================================================= -// IN-SCENE OVERLAY RENDERING VIA SUBMIT HOOK -//============================================================================= - -namespace -{ - struct IVRCompositor_Submit - { - static vr::EVRCompositorError thunk(vr::IVRCompositor* _this, vr::EVREye eEye, const vr::Texture_t* pTexture, const vr::VRTextureBounds_t* pBounds, vr::EVRSubmitFlags nSubmitFlags) - { - auto& vr = globals::features::vr; - // Only process DirectX textures - skip OpenGL/Vulkan to avoid undefined behavior - if (pTexture && pTexture->handle && pTexture->eType == vr::TextureType_DirectX) { - vr.RenderInSceneOverlay(eEye, (ID3D11Texture2D*)pTexture->handle, pBounds); - } - return func(_this, eEye, pTexture, pBounds, nSubmitFlags); - } - static inline REL::Relocation func; - }; -} - -void VR::InitInSceneResources() -{ - if (inSceneResources.initialized) - return; - - InSceneResources temp = {}; - - auto device = globals::d3d::device; - - // 1. Compile shaders - compile VS to get bytecode for input layout, PS separately - ID3DBlob* vsBlob = nullptr; - ID3DBlob* psBlob = nullptr; - ID3DBlob* errorBlob = nullptr; - - // Compile vertex shader - if (FAILED(D3DCompileFromFile(L"Data\\Shaders\\VR\\InSceneOverlay.vs.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, - "main", "vs_5_0", D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &vsBlob, &errorBlob))) { - if (errorBlob) { - logger::error("VR InScene VS compile error: {}", (char*)errorBlob->GetBufferPointer()); - errorBlob->Release(); - } - return; - } - if (errorBlob) { - errorBlob->Release(); - errorBlob = nullptr; - } - - // Compile pixel shader - if (FAILED(D3DCompileFromFile(L"Data\\Shaders\\VR\\InSceneOverlay.ps.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, - "main", "ps_5_0", D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &psBlob, &errorBlob))) { - if (errorBlob) { - logger::error("VR InScene PS compile error: {}", (char*)errorBlob->GetBufferPointer()); - errorBlob->Release(); - } - if (vsBlob) - vsBlob->Release(); - return; - } - if (errorBlob) { - errorBlob->Release(); - errorBlob = nullptr; - } - - // Create shader objects from bytecode - ID3D11VertexShader* vs = nullptr; - ID3D11PixelShader* ps = nullptr; - if (FAILED(device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, &vs)) || - FAILED(device->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, &ps))) { - logger::error("VR: Failed to create shader objects"); - if (vs) - vs->Release(); - if (ps) - ps->Release(); - if (vsBlob) - vsBlob->Release(); - if (psBlob) - psBlob->Release(); - return; - } - - temp.vs.attach(vs); - temp.ps.attach(ps); - if (psBlob) - psBlob->Release(); // Don't need PS blob anymore - - // 2. Input Layout - D3D11_INPUT_ELEMENT_DESC polygonLayout[2]; - polygonLayout[0].SemanticName = "POSITION"; - polygonLayout[0].SemanticIndex = 0; - polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT; - polygonLayout[0].InputSlot = 0; - polygonLayout[0].AlignedByteOffset = 0; - polygonLayout[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; - polygonLayout[0].InstanceDataStepRate = 0; - - polygonLayout[1].SemanticName = "TEXCOORD"; - polygonLayout[1].SemanticIndex = 0; - polygonLayout[1].Format = DXGI_FORMAT_R32G32_FLOAT; - polygonLayout[1].InputSlot = 0; - polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT; - polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; - polygonLayout[1].InstanceDataStepRate = 0; - - if (FAILED(device->CreateInputLayout(polygonLayout, 2, vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), temp.inputLayout.put()))) { - logger::error("VR: Failed to create input layout"); - vsBlob->Release(); - return; - } - - vsBlob->Release(); - - // 3. Buffers - // Quad Vertices (XY plane, z=0, size=1) - struct VertexType - { - XMFLOAT3 position; - XMFLOAT2 texture; - }; - VertexType vertices[4] = { - { XMFLOAT3(-0.5f, -0.5f, 0.0f), XMFLOAT2(0.0f, 1.0f) }, // Bottom Left - { XMFLOAT3(-0.5f, 0.5f, 0.0f), XMFLOAT2(0.0f, 0.0f) }, // Top Left - { XMFLOAT3(0.5f, 0.5f, 0.0f), XMFLOAT2(1.0f, 0.0f) }, // Top Right - { XMFLOAT3(0.5f, -0.5f, 0.0f), XMFLOAT2(1.0f, 1.0f) } // Bottom Right - }; - - D3D11_BUFFER_DESC vertexBufferDesc = {}; - vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT; - vertexBufferDesc.ByteWidth = sizeof(VertexType) * 4; - vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; - D3D11_SUBRESOURCE_DATA vertexData = {}; - vertexData.pSysMem = vertices; - if (FAILED(device->CreateBuffer(&vertexBufferDesc, &vertexData, temp.vb.put()))) { - logger::error("VR: Failed to create vertex buffer"); - return; - } - - unsigned long indices[6] = { 0, 1, 2, 0, 2, 3 }; - D3D11_BUFFER_DESC indexBufferDesc = {}; - indexBufferDesc.Usage = D3D11_USAGE_DEFAULT; - indexBufferDesc.ByteWidth = sizeof(unsigned long) * 6; - indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER; - D3D11_SUBRESOURCE_DATA indexData = {}; - indexData.pSysMem = indices; - if (FAILED(device->CreateBuffer(&indexBufferDesc, &indexData, temp.ib.put()))) { - logger::error("VR: Failed to create index buffer"); - return; - } - - D3D11_BUFFER_DESC matrixBufferDesc = {}; - matrixBufferDesc.Usage = D3D11_USAGE_DYNAMIC; - matrixBufferDesc.ByteWidth = sizeof(InSceneCB); - matrixBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; - matrixBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; - if (FAILED(device->CreateBuffer(&matrixBufferDesc, nullptr, temp.cb.put()))) { - logger::error("VR: Failed to create constant buffer"); - return; - } - - // 4. States - D3D11_BLEND_DESC blendDesc = {}; - blendDesc.RenderTarget[0].BlendEnable = TRUE; - blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; - blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; - blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; - blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; - blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO; - blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; - blendDesc.RenderTarget[0].RenderTargetWriteMask = 0x0F; - if (FAILED(device->CreateBlendState(&blendDesc, temp.blendState.put()))) { - logger::error("VR: Failed to create blend state"); - return; - } - - D3D11_DEPTH_STENCIL_DESC depthDesc = {}; - depthDesc.DepthEnable = FALSE; // Always on top - depthDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO; - depthDesc.DepthFunc = D3D11_COMPARISON_ALWAYS; - if (FAILED(device->CreateDepthStencilState(&depthDesc, temp.depthState.put()))) { - logger::error("VR: Failed to create depth stencil state"); - return; - } - - D3D11_RASTERIZER_DESC rasterDesc = {}; - rasterDesc.FillMode = D3D11_FILL_SOLID; - rasterDesc.CullMode = D3D11_CULL_NONE; - rasterDesc.FrontCounterClockwise = FALSE; - rasterDesc.DepthClipEnable = TRUE; - if (FAILED(device->CreateRasterizerState(&rasterDesc, temp.rasterizerState.put()))) { - logger::error("VR: Failed to create rasterizer state"); - return; - } - - D3D11_SAMPLER_DESC samplerDesc = {}; - samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; - samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; - samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; - samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; - samplerDesc.ComparisonFunc = D3D11_COMPARISON_NEVER; - samplerDesc.MinLOD = 0; - samplerDesc.MaxLOD = D3D11_FLOAT32_MAX; - if (FAILED(device->CreateSamplerState(&samplerDesc, temp.sampler.put()))) { - logger::error("VR: Failed to create sampler state"); - return; - } - - inSceneResources = std::move(temp); - inSceneResources.initialized = true; - logger::debug("VR: In-Scene Overlay resources initialized."); -} - -void VR::RenderInSceneOverlay(vr::EVREye eye, ID3D11Texture2D* targetTexture, const vr::VRTextureBounds_t* bounds) -{ - auto context = globals::d3d::context; - winrt::com_ptr perf; - context->QueryInterface(__uuidof(ID3DUserDefinedAnnotation), perf.put_void()); - - std::wstring eventName = L"VR In-Scene Overlay (Eye " + std::to_wstring((int)eye) + L")"; - if (perf) - perf->BeginEvent(eventName.c_str()); - - if (!inSceneResources.initialized) - InitInSceneResources(); - if (!inSceneResources.initialized) { - if (perf) - perf->EndEvent(); - return; - } - - // Only render if overlay should be visible - if (!globals::menu || !(globals::menu->IsEnabled || globals::menu->overlayVisible || settings.kAutoHideSeconds > 0)) { - if (perf) - perf->EndEvent(); - return; - } - if (!menuTexture) { - if (perf) - perf->EndEvent(); - return; - } - - // Skip rendering when attach mode is None (disabled) - if (settings.attachMode == AttachMode::None) { - if (perf) - perf->EndEvent(); - return; - } - - // We can't render if we don't have HMD pose - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - if (!openvr || !openvr->vrSystem) { - if (perf) - perf->EndEvent(); - return; - } - - // Get HMD Pose and Eye matrices - vr::TrackedDevicePose_t hmdPose; - vr::TrackedDevicePose_t renderPose[vr::k_unMaxTrackedDeviceCount]; - - RE::BSOpenVR::GetIVRCompositor()->GetLastPoses(renderPose, vr::k_unMaxTrackedDeviceCount, nullptr, 0); - hmdPose = renderPose[vr::k_unTrackedDeviceIndex_Hmd]; - if (!hmdPose.bPoseIsValid) { - if (perf) - perf->EndEvent(); - return; - } - - Matrix hmdWorld = Matrix::Identity; - Matrix eyeToHead = Matrix::Identity; - Matrix proj = Matrix::Identity; - Matrix vpHeadSpace = Matrix::Identity; // For HMD-relative rendering (head space) - Matrix vpWorldSpace = Matrix::Identity; // For world/controller rendering (world space) - - // Always get Eye and Projection matrices - eyeToHead = Util::HmdMatrix34ToMatrix(openvr->vrSystem->GetEyeToHeadTransform(eye)); - - // Use GetProjectionRaw to build a DirectX-compatible projection matrix (Depth [0, 1]) - // IMPORTANT: OpenVR GetProjectionRaw has a known bug (Valve issue #110, open since 2016): - // The 3rd parameter (named "pTop") actually returns the BOTTOM tangent, and - // the 4th parameter (named "pBottom") actually returns the TOP tangent. - // We name our variables to match the ACTUAL values, not the misleading parameter names. - float left, right, bottom, top; - openvr->vrSystem->GetProjectionRaw(eye, &left, &right, &bottom, &top); - float nearZ = 0.1f; - float farZ = 1000.0f; - - proj = DirectX::XMMatrixPerspectiveOffCenterRH(left * nearZ, right * nearZ, bottom * nearZ, top * nearZ, nearZ, farZ); - - // Log projection values once per eye - static bool projLogged[2] = { false, false }; - if (!projLogged[(int)eye]) { - logger::debug("VR Projection Eye {}: L={:.4f} R={:.4f} B={:.4f} T={:.4f}, EyeX={:.4f}", - (int)eye, left, right, bottom, top, eyeToHead._41); - projLogged[(int)eye] = true; - } - - // Head-space VP (for HMD-relative mode) - vpHeadSpace = eyeToHead.Invert() * proj; - - // World-space VP (for controller attach and fixed world position modes) - if (hmdPose.bPoseIsValid) { - hmdWorld = Util::HmdMatrix34ToMatrix(hmdPose.mDeviceToAbsoluteTracking); - Matrix eyeToWorld = hmdWorld * eyeToHead; - vpWorldSpace = eyeToWorld.Invert() * proj; - } - - // Create RTV for the target texture - winrt::com_ptr rtv; - D3D11_TEXTURE2D_DESC texDesc; - targetTexture->GetDesc(&texDesc); - - D3D11_RENDER_TARGET_VIEW_DESC rtvDesc = {}; - rtvDesc.Format = texDesc.Format; - - if (texDesc.ArraySize > 1) { - if (texDesc.SampleDesc.Count > 1) { - rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DMSARRAY; - rtvDesc.Texture2DMSArray.FirstArraySlice = (UINT)eye; - rtvDesc.Texture2DMSArray.ArraySize = 1; - } else { - rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY; - rtvDesc.Texture2DArray.FirstArraySlice = (UINT)eye; - rtvDesc.Texture2DArray.ArraySize = 1; - rtvDesc.Texture2DArray.MipSlice = 0; - } - } else if (texDesc.SampleDesc.Count > 1) { - rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DMS; - } else { - rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; - rtvDesc.Texture2D.MipSlice = 0; - } - - HRESULT hr = globals::d3d::device->CreateRenderTargetView(targetTexture, &rtvDesc, rtv.put()); - - if (FAILED(hr)) { - logger::error("VR: Failed to create RTV for eye texture (Format: {}, Samples: {}). HRESULT: {:x}", - (uint32_t)texDesc.Format, texDesc.SampleDesc.Count, (uint32_t)hr); - if (perf) - perf->EndEvent(); - return; - } - - // Save State - ID3D11RenderTargetView* oldRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT]; - ID3D11DepthStencilView* oldDSV; - context->OMGetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, oldRTVs, &oldDSV); - - D3D11_VIEWPORT oldViewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE]; - UINT numViewports = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE; - context->RSGetViewports(&numViewports, oldViewports); - - ID3D11RasterizerState* oldRS = nullptr; - context->RSGetState(&oldRS); - - // Setup Render - ID3D11RenderTargetView* rtvPtr = rtv.get(); - context->OMSetRenderTargets(1, &rtvPtr, nullptr); // No DSV - - // Viewport: Use bounds if provided (for SBS textures), otherwise use full texture - D3D11_VIEWPORT vpDesc = {}; - if (bounds) { - vpDesc.TopLeftX = bounds->uMin * texDesc.Width; - vpDesc.TopLeftY = bounds->vMin * texDesc.Height; - vpDesc.Width = (bounds->uMax - bounds->uMin) * texDesc.Width; - vpDesc.Height = (bounds->vMax - bounds->vMin) * texDesc.Height; - } else { - vpDesc.TopLeftX = 0.0f; - vpDesc.TopLeftY = 0.0f; - vpDesc.Width = (float)texDesc.Width; - vpDesc.Height = (float)texDesc.Height; - } - vpDesc.MinDepth = 0.0f; - vpDesc.MaxDepth = 1.0f; - context->RSSetViewports(1, &vpDesc); - - // Log texture and viewport details once per session - static bool textureInfoLogged = false; - if (!textureInfoLogged) { - logger::debug("VR Submit Texture Info:"); - logger::debug(" Texture Size: {}x{}, Format: {}, ArraySize: {}, SampleCount: {}", - texDesc.Width, texDesc.Height, (uint32_t)texDesc.Format, texDesc.ArraySize, texDesc.SampleDesc.Count); - if (bounds) { - logger::debug(" Bounds for Eye {}: uMin={:.3f}, vMin={:.3f}, uMax={:.3f}, vMax={:.3f}", - (int)eye, bounds->uMin, bounds->vMin, bounds->uMax, bounds->vMax); - logger::debug(" Viewport: X={:.0f}, Y={:.0f}, W={:.0f}, H={:.0f}", - vpDesc.TopLeftX, vpDesc.TopLeftY, vpDesc.Width, vpDesc.Height); - } else { - logger::debug(" No bounds provided (full texture per eye, or texture array)"); - logger::debug(" Viewport: X={:.0f}, Y={:.0f}, W={:.0f}, H={:.0f}", - vpDesc.TopLeftX, vpDesc.TopLeftY, vpDesc.Width, vpDesc.Height); - } - logger::debug(" RTV Dimension: {}", - rtvDesc.ViewDimension == D3D11_RTV_DIMENSION_TEXTURE2DARRAY ? "Texture2DArray (per-eye slice)" : - rtvDesc.ViewDimension == D3D11_RTV_DIMENSION_TEXTURE2D ? "Texture2D (single)" : - rtvDesc.ViewDimension == D3D11_RTV_DIMENSION_TEXTURE2DMS ? "Texture2DMS" : - rtvDesc.ViewDimension == D3D11_RTV_DIMENSION_TEXTURE2DMSARRAY ? "Texture2DMSArray" : - "Unknown"); - // Log again for the other eye - if (eye == vr::Eye_Right) - textureInfoLogged = true; - } - - // Helper to draw the overlay quad with a given WVP matrix - auto drawOverlayQuad = [&](ID3D11DeviceContext* ctx, const InSceneCB& cbData) { - D3D11_MAPPED_SUBRESOURCE mappedResource; - if (SUCCEEDED(ctx->Map(inSceneResources.cb.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource))) { - memcpy(mappedResource.pData, &cbData, sizeof(InSceneCB)); - ctx->Unmap(inSceneResources.cb.get(), 0); - } - - ctx->VSSetShader(inSceneResources.vs.get(), nullptr, 0); - ctx->PSSetShader(inSceneResources.ps.get(), nullptr, 0); - ID3D11Buffer* cb = inSceneResources.cb.get(); - ctx->VSSetConstantBuffers(0, 1, &cb); - - struct VT - { - XMFLOAT3 p; - XMFLOAT2 t; - }; - UINT stride = sizeof(VT); - UINT offset = 0; - ID3D11Buffer* vb = inSceneResources.vb.get(); - ctx->IASetVertexBuffers(0, 1, &vb, &stride, &offset); - ctx->IASetIndexBuffer(inSceneResources.ib.get(), DXGI_FORMAT_R32_UINT, 0); - ctx->IASetInputLayout(inSceneResources.inputLayout.get()); - ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - - ctx->OMSetBlendState(inSceneResources.blendState.get(), nullptr, 0xFFFFFFFF); - ctx->OMSetDepthStencilState(inSceneResources.depthState.get(), 0); - ctx->RSSetState(inSceneResources.rasterizerState.get()); - - // Cache SRV to avoid creating every frame - if (menuTexture.get() != inSceneResources.cachedMenuTexture) { - inSceneResources.menuSRV = nullptr; - if (FAILED(globals::d3d::device->CreateShaderResourceView(menuTexture.get(), nullptr, inSceneResources.menuSRV.put()))) { - logger::error("VR: Failed to create menu texture SRV"); - return; - } - inSceneResources.cachedMenuTexture = menuTexture.get(); - } - ID3D11ShaderResourceView* srvPtr = inSceneResources.menuSRV.get(); - ctx->PSSetShaderResources(0, 1, &srvPtr); - - ID3D11SamplerState* sampler = inSceneResources.sampler.get(); - ctx->PSSetSamplers(0, 1, &sampler); - - ctx->DrawIndexed(6, 0, 0); - }; - - // --- Render HMD Overlay --- - if ((settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) && menuTexture) { - InSceneCB cbData; - - Matrix modelMatrix; - Matrix vp; - if (settings.VRMenuPositioningMethod == 1) { // Fixed World Position - modelMatrix = CreateOverlayScaleMatrix(settings.VRMenuScale) * fixedWorldOverlayPosition.m; - vp = vpWorldSpace; - } else { // HMD Relative - Matrix offset = Matrix::CreateTranslation(settings.VRMenuOffsetX, settings.VRMenuOffsetY, settings.VRMenuOffsetZ); - modelMatrix = CreateOverlayScaleMatrix(settings.VRMenuScale) * offset; - vp = vpHeadSpace; - } - cbData.wvp = (modelMatrix * vp).Transpose(); - - drawOverlayQuad(context, cbData); - } - - // --- Render Controller Overlay --- - if ((settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) && menuTexture) { - vr::TrackedDeviceIndex_t attachIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - if (attachIndex != vr::k_unTrackedDeviceIndexInvalid && attachIndex < vr::k_unMaxTrackedDeviceCount) { - vr::TrackedDevicePose_t controllerPose = renderPose[attachIndex]; - if (controllerPose.bPoseIsValid) { - Matrix controllerWorld = Util::HmdMatrix34ToMatrix(controllerPose.mDeviceToAbsoluteTracking); - Matrix offset = Matrix::CreateTranslation(settings.VRMenuControllerOffsetX, settings.VRMenuControllerOffsetY, settings.VRMenuControllerOffsetZ); - Matrix modelMatrix = CreateOverlayScaleMatrix(settings.VRMenuScale) * offset * controllerWorld; - - // Backface culling: hide overlay when viewed from behind - // Use the unscaled controller+offset transform for correct normal direction - Matrix overlayTransform = offset * controllerWorld; - Vector3 overlayNormal(overlayTransform._31, overlayTransform._32, overlayTransform._33); - overlayNormal.Normalize(); - Matrix eyeWorld = hmdWorld * eyeToHead; - Vector3 eyePos = eyeWorld.Translation(); - Vector3 overlayPos = overlayTransform.Translation(); - Vector3 toEye = eyePos - overlayPos; - toEye.Normalize(); - // Quad front face is +Z in local space (D3D default CW winding). - // Render when eye is on the +Z side of the overlay (dot > 0). - float dot = overlayNormal.Dot(toEye); - if (dot > 0.0f) { - InSceneCB cbData; - cbData.wvp = (modelMatrix * vpWorldSpace).Transpose(); - drawOverlayQuad(context, cbData); - } - } - } - } - - // Restore State - context->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, oldRTVs, oldDSV); - context->RSSetViewports(numViewports, oldViewports); - if (oldRS) { - context->RSSetState(oldRS); - oldRS->Release(); - } - for (int i = 0; i < D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT; ++i) - if (oldRTVs[i]) - oldRTVs[i]->Release(); - if (oldDSV) - oldDSV->Release(); - - if (perf) - perf->EndEvent(); -} - -void VR::InstallSubmitHook() -{ - static bool installed = false; - if (installed) - return; - - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - if (openvr && RE::BSOpenVR::GetIVRCompositor()) { - logger::info("VR: Installing IVRCompositor::Submit hook for in-scene overlay rendering"); - - // Log comprehensive VR system parameters (debug only) - logger::debug("=== VR System Configuration ==="); - - // Get and log IPD - float ipd = Util::GetIPDFromHMD(); - logger::debug("IPD: {:.4f} meters ({:.2f} mm)", ipd, ipd * 1000.0f); - - // Get and log eye transforms - if (openvr->vrSystem) { - vr::HmdMatrix34_t leftEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Left); - vr::HmdMatrix34_t rightEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Right); - - logger::debug("Left Eye Transform:"); - logger::debug(" Translation: X={:.4f}, Y={:.4f}, Z={:.4f}", - leftEye.m[0][3], leftEye.m[1][3], leftEye.m[2][3]); - logger::debug("Right Eye Transform:"); - logger::debug(" Translation: X={:.4f}, Y={:.4f}, Z={:.4f}", - rightEye.m[0][3], rightEye.m[1][3], rightEye.m[2][3]); - logger::debug("Calculated Eye Separation: {:.4f} meters ({:.2f} mm)", - std::abs(leftEye.m[0][3] - rightEye.m[0][3]), - std::abs(leftEye.m[0][3] - rightEye.m[0][3]) * 1000.0f); - - // Get projection matrices - vr::HmdMatrix44_t leftProj = openvr->vrSystem->GetProjectionMatrix(vr::Eye_Left, 0.1f, 1000.0f); - vr::HmdMatrix44_t rightProj = openvr->vrSystem->GetProjectionMatrix(vr::Eye_Right, 0.1f, 1000.0f); - - logger::debug("Projection Matrices (near=0.1, far=1000.0):"); - logger::debug(" Left [0][0]={:.4f}, [1][1]={:.4f}, [0][2]={:.4f}", - leftProj.m[0][0], leftProj.m[1][1], leftProj.m[0][2]); - logger::debug(" Right [0][0]={:.4f}, [1][1]={:.4f}, [0][2]={:.4f}", - rightProj.m[0][0], rightProj.m[1][1], rightProj.m[0][2]); - } - - logger::debug("Convergence Formula Info:"); - logger::debug(" Formula: stereoShift = (IPD/2) / (depth * tan(hFOV/2))"); - logger::debug(" - Shift is independent of scale (scale only controls size)"); - logger::debug(" - Depth is controlled by OffsetZ (negative = in front)"); - float halfIPD = ipd / 2.0f; - if (openvr->vrSystem) { - vr::HmdMatrix44_t proj = openvr->vrSystem->GetProjectionMatrix(vr::Eye_Left, 0.1f, 1000.0f); - float tanHFOV = 1.0f / proj.m[0][0]; - logger::debug(" tan(hFOV/2) = {:.4f}", tanHFOV); - logger::debug(" Example: At depth 1.0m, shift={:.6f}", halfIPD / (1.0f * tanHFOV)); - logger::debug(" Example: At depth 2.0m, shift={:.6f}", halfIPD / (2.0f * tanHFOV)); - logger::debug(" Example: At depth 5.0m, shift={:.6f}", halfIPD / (5.0f * tanHFOV)); - } - logger::debug("================================"); - - // IVRCompositor::Submit is index 5 - stl::detour_vfunc<5, IVRCompositor_Submit>(RE::BSOpenVR::GetIVRCompositor()); - installed = true; - - logger::info("VR: In-scene overlay initialized"); - } else { - logger::warn("VR: Failed to install IVRCompositor::Submit hook - Interface not available"); - } -} diff --git a/src/Features/VR/Input.cpp b/src/Features/VR/Input.cpp deleted file mode 100644 index 8105b42e30..0000000000 --- a/src/Features/VR/Input.cpp +++ /dev/null @@ -1,376 +0,0 @@ -#include "Features/VR.h" -#include "Menu.h" -#include "Utils/PerfUtils.h" -#include "Utils/VRUtils.h" - -#include - -using AttachMode = VR::Settings::OverlayAttachMode; - -void VR::UpdateOverlayMenuStateFromInput() -{ - if (this->isCapturingCombo) { - return; - } - - if (globals::menu == nullptr) - return; - - bool& isEnabled = globals::menu->IsEnabled; - bool& overlayEnabled = globals::menu->overlayVisible; - bool& testMode = settings.VRMenuControllerDiagnosticsTestMode; - - if (testMode) { - if (!isEnabled) { - settings.VRMenuControllerDiagnosticsTestMode = false; - return; - } - return; - } - - bool uiMenusOpen = globals::game::ui && - (globals::game::ui->IsMenuOpen(RE::MainMenu::MENU_NAME) || globals::game::ui->IsMenuOpen(RE::TweenMenu::MENU_NAME)); - - bool inValidMenuState = uiMenusOpen || (globals::game::ui && (isEnabled || overlayEnabled)); - - if (!inValidMenuState) - return; - - struct MenuStateMapping - { - std::function condition; - std::function action; - bool allowWhenUIMenusClosed = false; - }; - - auto CheckCombo = [&](const std::vector& combos) -> bool { - if (combos.empty()) - return false; - - for (size_t i = 0; i < combos.size(); ++i) { - const auto& combo = combos[i]; - bool buttonPressed = false; - - switch (combo.GetDevice()) { - case ControllerDevice::Both: - buttonPressed = primaryControllerState[combo.GetKey()].isPressed && - secondaryControllerState[combo.GetKey()].isPressed; - break; - case ControllerDevice::Primary: - buttonPressed = primaryControllerState[combo.GetKey()].isPressed; - break; - case ControllerDevice::Secondary: - buttonPressed = secondaryControllerState[combo.GetKey()].isPressed; - break; - } - - if (!buttonPressed) { - return false; - } - } - - return true; - }; - - std::vector mappings = { - // Open Community Shaders menu when closed - { [&]() { - return CheckCombo(settings.VRMenuOpenKeys) && !isEnabled; - }, - [&]() { isEnabled = true; } }, - - // Close Community Shaders menu when open - { [&]() { - return CheckCombo(settings.VRMenuCloseKeys) && isEnabled; - }, - [&]() { - isEnabled = false; - overlayDragState.dragging = false; - }, - true }, - - // Open VR overlay when closed (only when CS menu is open) - { [&]() { - return CheckCombo(settings.VROverlayOpenKeys) && !overlayEnabled && isEnabled; - }, - [&]() { overlayEnabled = true; } }, - - // Close VR overlay when open (only when CS menu is open) - { [&]() { - return CheckCombo(settings.VROverlayCloseKeys) && overlayEnabled && isEnabled; - }, - [&]() { overlayEnabled = false; } } - }; - - bool onlyAllowClose = isEnabled && !uiMenusOpen; - - for (const auto& mapping : mappings) { - if (onlyAllowClose && !mapping.allowWhenUIMenusClosed) - continue; - - if (mapping.condition()) { - mapping.action(); - break; - } - } -} - -void VR::ProcessVREvents(std::vector& vrEvents) -{ - bool currentLeftHandedMode = RE::BSOpenVRControllerDevice::IsLeftHandedMode(); - static bool firstCall = true; - if (firstCall || currentLeftHandedMode != lastKnownLeftHandedMode) { - if (!firstCall) { - logger::debug("VR handedness changed: {} -> {}", lastKnownLeftHandedMode ? "Left" : "Right", currentLeftHandedMode ? "Left" : "Right"); - } - firstCall = false; - lastKnownLeftHandedMode = currentLeftHandedMode; - primaryControllerState = {}; - secondaryControllerState = {}; - } - - double nowSecs = Util::GetNowSecs(); - for (auto& event : vrEvents) { - bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); - bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); - struct VRButtonDescriptor - { - const char* name; - bool (*isButton)(std::uint32_t); - std::uint32_t keyCode; - }; - static const VRButtonDescriptor kVRButtons[] = { - { "Grip", RE::BSOpenVRControllerDevice::IsGripButton, RE::BSOpenVRControllerDevice::Keys::kGrip }, - { "GripAlt", RE::BSOpenVRControllerDevice::IsGripButton, RE::BSOpenVRControllerDevice::Keys::kGripAlt }, - { "Trigger", RE::BSOpenVRControllerDevice::IsTriggerButton, RE::BSOpenVRControllerDevice::Keys::kTrigger }, - { "Stick Click", RE::BSOpenVRControllerDevice::IsStickClick, RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger }, - { "Touchpad Click", RE::BSOpenVRControllerDevice::IsTouchpadClick, RE::BSOpenVRControllerDevice::Keys::kTouchpadClick }, - { "Touchpad Alt", RE::BSOpenVRControllerDevice::IsTouchpadClick, RE::BSOpenVRControllerDevice::Keys::kTouchpadAlt }, - { "A/X", RE::BSOpenVRControllerDevice::IsAButton, RE::BSOpenVRControllerDevice::Keys::kXA }, - { "B/Y", RE::BSOpenVRControllerDevice::IsBButton, RE::BSOpenVRControllerDevice::Keys::kBY }, - }; - for (const auto& desc : kVRButtons) { - if (event.keyCode == desc.keyCode) { - RE::ButtonState* state = isPrimary ? &primaryControllerState[desc.keyCode] : isSecondary ? &secondaryControllerState[desc.keyCode] : - nullptr; - if (state) { - state->OnEvent(event.IsPressed(), nowSecs); - } - break; - } - } - switch (event.eventType) { - case RE::INPUT_EVENT_TYPE::kButton: - ProcessVRButtonEvent(event); - break; - case RE::INPUT_EVENT_TYPE::kThumbstick: - UpdateControllerState(event); - break; - default: - break; - } - } -} - -void VR::ProcessVRButtonEvent(const Menu::KeyEvent& event) -{ - if (this->isCapturingCombo) { - return; - } - - ImGuiIO& io = ImGui::GetIO(); - bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); - bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); - bool& testMode = settings.VRMenuControllerDiagnosticsTestMode; - constexpr size_t kNumTriggerMappings = 1; - - if (isPrimary || isSecondary) { - constexpr size_t kNumMappings = 6; - RE::ButtonMapping mappings[kNumMappings] = { - { RE::BSOpenVRControllerDevice::Keys::kTrigger, ImGuiMouseButton_Left, false, ImGuiKey_None, false }, - { RE::BSOpenVRControllerDevice::Keys::kGrip, ImGuiMouseButton_Right, false, ImGuiKey_None, false }, - { RE::BSOpenVRControllerDevice::Keys::kTouchpadClick, ImGuiMouseButton_Middle, false, ImGuiKey_None, false }, - { RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger, ImGuiMouseButton_Middle, false, ImGuiKey_None, false }, - { RE::BSOpenVRControllerDevice::Keys::kBY, -1, true, Util::Input::VirtualKeyToImGuiKey(VK_TAB), isSecondary }, - { RE::BSOpenVRControllerDevice::Keys::kXA, -1, true, Util::Input::VirtualKeyToImGuiKey(VK_RETURN), false }, - }; - - static bool prevPrimaryStates[kNumMappings] = {}; - static bool prevSecondaryStates[kNumMappings] = {}; - bool* prevStates = isPrimary ? prevPrimaryStates : prevSecondaryStates; - - RE::InputDeviceState& controllerState = isPrimary ? primaryControllerState : secondaryControllerState; - - size_t limit = testMode ? kNumTriggerMappings : kNumMappings; - - for (size_t i = 0; i < limit; ++i) { - RE::ButtonState* state = &controllerState[mappings[i].keyCode]; - bool curr = state ? state->isPressed : false; - if (curr != prevStates[i]) { - if (mappings[i].isKeyEvent) { - if (mappings[i].isShift) - io.AddKeyEvent(ImGuiMod_Shift, curr); - io.AddKeyEvent(static_cast(mappings[i].key), curr); - } else { - io.AddMouseButtonEvent(mappings[i].logicalButton, curr); - } - prevStates[i] = curr; - } - } - } - - VRControllerEventLog logEntry; - logEntry.device = static_cast(event.device); - logEntry.keyCode = event.keyCode; - logEntry.value = static_cast(event.value); - logEntry.pressed = event.IsPressed(); - logEntry.heldTime = 0.0; - logEntry.heldSource = "button"; - logEntry.thumbstickX = 0.0f; - logEntry.thumbstickY = 0.0f; - logEntry.controllerRole = isPrimary ? "Primary" : isSecondary ? "Secondary" : - "Unknown"; - vrControllerEventLog.push_back(logEntry); - if (vrControllerEventLog.size() > 32) { - vrControllerEventLog.erase(vrControllerEventLog.begin()); - } -} - -void VR::UpdateControllerState(const Menu::KeyEvent& event) -{ - bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); - bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); - - if (isPrimary) { - primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x = event.thumbstickX; - primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y = event.thumbstickY; - } else if (isSecondary) { - secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x = event.thumbstickX; - secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y = event.thumbstickY; - } - - VRControllerEventLog logEntry; - logEntry.device = static_cast(event.device); - logEntry.keyCode = event.keyCode; - logEntry.value = static_cast(event.value); - logEntry.pressed = event.IsPressed(); - logEntry.heldTime = 0.0; - logEntry.heldSource = "thumbstick"; - logEntry.thumbstickX = event.thumbstickX; - logEntry.thumbstickY = event.thumbstickY; - logEntry.controllerRole = isPrimary ? "Primary" : "Secondary"; - vrControllerEventLog.push_back(logEntry); - if (vrControllerEventLog.size() > 32) { - vrControllerEventLog.erase(vrControllerEventLog.begin()); - } -} - -void VR::ProcessThumbstickScroll(RE::VRControllerState& controllerState, size_t thumbstickIndex, float deadzone, ImGuiIO& io) -{ - bool usingScrollStickX = (std::abs(controllerState.thumbsticks[thumbstickIndex].x) > deadzone); - bool usingScrollStickY = (std::abs(controllerState.thumbsticks[thumbstickIndex].y) > deadzone); - - if (usingScrollStickX || usingScrollStickY) { - struct ScrollAccum - { - float x = 0.0f; - float y = 0.0f; - }; - static std::unordered_map scrollAccums; - ScrollAccum& accum = scrollAccums[thumbstickIndex]; - - accum.x += controllerState.thumbsticks[thumbstickIndex].x * 0.1f; - accum.y += controllerState.thumbsticks[thumbstickIndex].y * 0.1f; - - float scrollEventX = 0.0f; - float scrollEventY = 0.0f; - - if (std::abs(accum.x) > 0.3f) { - scrollEventX = accum.x > 0 ? 1.0f : -1.0f; - accum.x = 0.0f; - } - if (std::abs(accum.y) > 0.3f) { - scrollEventY = accum.y > 0 ? 1.0f : -1.0f; - accum.y = 0.0f; - } - - if (scrollEventX != 0.0f || scrollEventY != 0.0f) { - io.AddMouseWheelEvent(-scrollEventX, scrollEventY); - } - } -} - -void VR::ProcessControllerInputForImGui() -{ - if (!globals::menu || !globals::menu->IsEnabled) - return; - bool testMode = settings.VRMenuControllerDiagnosticsTestMode; - float mouseDeadzone = settings.mouseDeadzone; - float mouseSpeed = settings.mouseSpeed; - ImGuiIO& io = ImGui::GetIO(); - io.ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; - io.WantSetMousePos = false; - - bool wandHandledCursor = false; - if (!testMode && settings.EnableWandPointing) { - UpdateCursorFromWandPointing(); - wandHandledCursor = wandState.isIntersecting; - } - - if (!testMode) { - bool isDragging = overlayDragState.dragging; - - if (wandHandledCursor && !isDragging) { - ProcessThumbstickScroll(primaryControllerState, static_cast(RE::ControllerRole::Primary), mouseDeadzone, io); - ProcessThumbstickScroll(secondaryControllerState, static_cast(RE::ControllerRole::Secondary), mouseDeadzone, io); - } else if (!isDragging) { - bool useAttachedControllerForCursor = (settings.attachMode == VR::Settings::OverlayAttachMode::ControllerOnly || - settings.attachMode == VR::Settings::OverlayAttachMode::Both); - - RE::VRControllerState* cursorController = nullptr; - RE::VRControllerState* scrollController = nullptr; - - if (useAttachedControllerForCursor) { - if (settings.VRMenuAttachController == ControllerDevice::Primary) { - cursorController = &primaryControllerState; - scrollController = &secondaryControllerState; - } else { - cursorController = &secondaryControllerState; - scrollController = &primaryControllerState; - } - } else { - cursorController = &primaryControllerState; - scrollController = &secondaryControllerState; - } - - if (cursorController) { - size_t thumbstickIndex = (cursorController == &primaryControllerState) ? - static_cast(RE::ControllerRole::Primary) : - static_cast(RE::ControllerRole::Secondary); - - float thumbstickX = cursorController->thumbsticks[thumbstickIndex].x; - float thumbstickY = cursorController->thumbsticks[thumbstickIndex].y; - bool usingCursorStick = (std::abs(thumbstickX) > mouseDeadzone || std::abs(thumbstickY) > mouseDeadzone); - - if (usingCursorStick) { - ImVec2 mousePos = io.MousePos; - mousePos.x += thumbstickX * mouseSpeed; - mousePos.y -= thumbstickY * mouseSpeed; - mousePos.x = std::clamp(mousePos.x, 0.0f, io.DisplaySize.x); - mousePos.y = std::clamp(mousePos.y, 0.0f, io.DisplaySize.y); - io.MousePos = mousePos; - io.AddMousePosEvent(mousePos.x, mousePos.y); - io.MouseDrawCursor = true; - io.WantSetMousePos = true; - } - } - - if (scrollController) { - size_t thumbstickIndex = (scrollController == &primaryControllerState) ? - static_cast(RE::ControllerRole::Primary) : - static_cast(RE::ControllerRole::Secondary); - ProcessThumbstickScroll(*scrollController, thumbstickIndex, mouseDeadzone, io); - } - } - } -} diff --git a/src/Features/VR/OpenVRDetection.cpp b/src/Features/VR/OpenVRDetection.cpp deleted file mode 100644 index 78223d28b9..0000000000 --- a/src/Features/VR/OpenVRDetection.cpp +++ /dev/null @@ -1,136 +0,0 @@ -#include "OpenVRDetection.h" -#include -#include -#include -#include -#include -#pragma comment(lib, "version.lib") - -namespace VRDetection -{ - const char* RuntimeTypeToString(RuntimeType type) - { - switch (type) { - case RuntimeType::SteamVR: - return "SteamVR"; - case RuntimeType::OpenComposite: - return "OpenComposite"; - default: - return "Unknown"; - } - } - - bool ProbeRuntimeInterfaces(OpenVRDetectionResult& result) - { - HMODULE hModule = GetModuleHandleA("openvr_api.dll"); - if (!hModule) - return false; - - using pfnIsValid = bool(__cdecl*)(const char*); - auto IsValid = reinterpret_cast(GetProcAddress(hModule, "VR_IsInterfaceVersionValid")); - if (!IsValid) - return false; - - result.hasOverlayInterface = IsValid(vr::IVROverlay_Version); - result.hasSystemInterface = IsValid(vr::IVRSystem_Version); - result.hasCompositorInterface = IsValid(vr::IVRCompositor_Version); - - result.probingSucceeded = result.hasOverlayInterface && result.hasSystemInterface && result.hasCompositorInterface; - return result.probingSucceeded; - } - - void GatherDLLInfo(OpenVRDetectionResult& result) - { - HMODULE hModule = GetModuleHandleA("openvr_api.dll"); - if (!hModule) { - result.isAvailable = false; - return; - } - - result.isAvailable = true; - - char dllPath[MAX_PATH]; - DWORD fileLength = GetModuleFileNameA(hModule, dllPath, MAX_PATH); - if (fileLength == 0 || (fileLength == MAX_PATH && GetLastError() == ERROR_INSUFFICIENT_BUFFER)) { - result.isAvailable = false; - return; - } - - result.dllPath = dllPath; - - DWORD dwSize = GetFileVersionInfoSizeA(dllPath, nullptr); - if (dwSize > 0) { - std::vector buffer(dwSize); - if (GetFileVersionInfoA(dllPath, 0, dwSize, buffer.data())) { - VS_FIXEDFILEINFO* pFileInfo = nullptr; - UINT len = 0; - if (VerQueryValueA(buffer.data(), "\\", reinterpret_cast(&pFileInfo), &len)) { - DWORD major = HIWORD(pFileInfo->dwFileVersionMS); - DWORD minor = LOWORD(pFileInfo->dwFileVersionMS); - DWORD build = HIWORD(pFileInfo->dwFileVersionLS); - DWORD revision = LOWORD(pFileInfo->dwFileVersionLS); - result.version = std::format("{}.{}.{}.{}", major, minor, build, revision); - } - } - } - - if (result.version.empty()) - result.version = "Unknown"; - - WIN32_FIND_DATAA findData; - HANDLE hFind = FindFirstFileA(dllPath, &findData); - if (hFind != INVALID_HANDLE_VALUE) { - FindClose(hFind); - ULARGE_INTEGER fileSize; - fileSize.LowPart = findData.nFileSizeLow; - fileSize.HighPart = findData.nFileSizeHigh; - result.fileSize = fileSize.QuadPart; - - SYSTEMTIME st; - FileTimeToSystemTime(&findData.ftLastWriteTime, &st); - result.modificationTime = std::format("{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}", - st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); - } - } - - RuntimeType DetectRuntimeType(const std::string& dllPath, const std::string& version, uint64_t fileSize) - { - // OpenComposite DLLs are typically small (~600KB) with version 1.0.10.0 - if (version == "1.0.10.0" && fileSize < 700000) - return RuntimeType::OpenComposite; - - // Check path for OpenComposite indicators - std::string lowerPath = dllPath; - for (auto& c : lowerPath) - c = static_cast(std::tolower(static_cast(c))); - - if (lowerPath.find("opencomposite") != std::string::npos) - return RuntimeType::OpenComposite; - - // SteamVR DLLs are typically larger and have higher version numbers - if (lowerPath.find("steamvr") != std::string::npos || lowerPath.find("steam") != std::string::npos) - return RuntimeType::SteamVR; - - // Higher version numbers suggest SteamVR - if (!version.empty() && version != "Unknown" && version != "1.0.10.0") - return RuntimeType::SteamVR; - - return RuntimeType::Unknown; - } - - OpenVRDetectionResult Detect() - { - OpenVRDetectionResult result; - - GatherDLLInfo(result); - if (!result.isAvailable) - return result; - - result.runtimeType = DetectRuntimeType(result.dllPath, result.version, result.fileSize); - - // Detect compatibility via runtime interface probing - result.isCompatible = ProbeRuntimeInterfaces(result); - - return result; - } -} diff --git a/src/Features/VR/OpenVRDetection.h b/src/Features/VR/OpenVRDetection.h deleted file mode 100644 index 1eaa411272..0000000000 --- a/src/Features/VR/OpenVRDetection.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once -#include -#include - -namespace VRDetection -{ - enum class RuntimeType - { - Unknown, - SteamVR, - OpenComposite - }; - - struct OpenVRDetectionResult - { - bool isAvailable = false; - bool isCompatible = false; - - // Interface probing results - bool hasOverlayInterface = false; - bool hasSystemInterface = false; - bool hasCompositorInterface = false; - - // File-based info - std::string dllPath; - std::string version; - uint64_t fileSize = 0; - std::string modificationTime; - - // Detection metadata - RuntimeType runtimeType = RuntimeType::Unknown; - bool probingSucceeded = false; - }; - - // Runtime interface probing via VR_IsInterfaceVersionValid - bool ProbeRuntimeInterfaces(OpenVRDetectionResult& result); - - // Gather DLL metadata (path, version, size, timestamp) - void GatherDLLInfo(OpenVRDetectionResult& result); - - // Detect runtime type (SteamVR vs OpenComposite) - RuntimeType DetectRuntimeType(const std::string& dllPath, const std::string& version, uint64_t fileSize); - - // Full detection via interface probing - OpenVRDetectionResult Detect(); - - const char* RuntimeTypeToString(RuntimeType type); -} diff --git a/src/Features/VR/OverlayDrag.cpp b/src/Features/VR/OverlayDrag.cpp deleted file mode 100644 index e5912e8206..0000000000 --- a/src/Features/VR/OverlayDrag.cpp +++ /dev/null @@ -1,405 +0,0 @@ -#include "Features/VR.h" -#include "RE/B/BSOpenVR.h" -#include "RE/P/PlayerCharacter.h" -#include "Utils/VRUtils.h" - -#include -#include -#include -#include -#include - -using namespace DirectX::SimpleMath; -using AttachMode = VR::Settings::OverlayAttachMode; - -bool VR::GetGripPressed(bool isLeft, bool isRight) const -{ - bool isLeftHanded = lastKnownLeftHandedMode; - - if (isLeft) { - if (isLeftHanded) { - return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } else { - return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } - } - if (isRight) { - if (isLeftHanded) { - return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } else { - return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } - } - return false; -} - -static bool CanStartAny(vr::ETrackedControllerRole role) -{ - return role != vr::TrackedControllerRole_Invalid; -} - -void VR::UpdateOverlayDrag() -{ - if (!CanPerformDrag()) { - return; - } - - if (overlayDragState.dragging) { - UpdateActiveDrag(); - } else { - TryStartNewDrag(); - } -} - -bool VR::CanPerformDrag() -{ - if (!settings.EnableDragToReposition) - return false; - - if (!globals::menu || !globals::menu->IsEnabled) - return false; - - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - auto* system = openvr ? openvr->vrSystem : nullptr; - if (!system) - return false; - - if (settings.VRMenuControllerDiagnosticsTestMode) { - return false; - } - - return true; -} - -void VR::UpdateActiveDrag() -{ - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - auto* system = openvr ? openvr->vrSystem : nullptr; - if (!system) - return; - - auto resetDragState = [&]() { - overlayDragState.dragging = false; - overlayDragState.controllerIndex = vr::k_unTrackedDeviceIndexInvalid; - overlayDragState.isPrimary = false; - overlayDragState.isSecondary = false; - }; - - float rawMatrix[3][4]; - if (Util::GetControllerWorldMatrix(overlayDragState.controllerIndex, rawMatrix)) { - vr::HmdMatrix34_t mat = Util::Float3x4ToHmdMatrix34(rawMatrix); - Matrix controllerMatrix = Util::HmdMatrix34ToMatrix(mat); - - switch (overlayDragState.mode) { - case OverlayDragState::DragMode::Controller: - { - vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - - if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { - vr::TrackedDevicePose_t controllerPose; - if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &controllerPose, 1)) - break; - if (controllerPose.bPoseIsValid) { - Matrix attachedControllerMatrix = Util::HmdMatrix34ToMatrix(controllerPose.mDeviceToAbsoluteTracking); - - Vector3 worldDelta( - controllerMatrix._41 - overlayDragState.initialControllerMatrix._41, - controllerMatrix._42 - overlayDragState.initialControllerMatrix._42, - controllerMatrix._43 - overlayDragState.initialControllerMatrix._43); - - Matrix worldToLocal = attachedControllerMatrix.Invert(); - Vector3 localDelta = Vector3::TransformNormal(worldDelta, worldToLocal); - - settings.VRMenuControllerOffsetX = overlayDragState.initialControllerOffset.x + localDelta.x; - settings.VRMenuControllerOffsetY = overlayDragState.initialControllerOffset.y + localDelta.y; - settings.VRMenuControllerOffsetZ = overlayDragState.initialControllerOffset.z + localDelta.z; - } - } - break; - } - case OverlayDragState::DragMode::FixedWorld: - { - Vector3 worldDelta( - controllerMatrix._41 - overlayDragState.initialControllerMatrix._41, - controllerMatrix._42 - overlayDragState.initialControllerMatrix._42, - controllerMatrix._43 - overlayDragState.initialControllerMatrix._43); - Matrix translated = overlayDragState.initialOverlayMatrix; - translated._41 += worldDelta.x; - translated._42 += worldDelta.y; - translated._43 += worldDelta.z; - fixedWorldOverlayPosition.m = translated; - break; - } - case OverlayDragState::DragMode::HMD: - { - vr::TrackedDevicePose_t hmdPose; - if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &hmdPose, 1)) - break; - if (hmdPose.bPoseIsValid) { - Matrix hmdMatrix = Util::HmdMatrix34ToMatrix(hmdPose.mDeviceToAbsoluteTracking); - - Vector3 worldDelta( - controllerMatrix._41 - overlayDragState.initialControllerMatrix._41, - controllerMatrix._42 - overlayDragState.initialControllerMatrix._42, - controllerMatrix._43 - overlayDragState.initialControllerMatrix._43); - - Matrix worldToLocal = hmdMatrix.Invert(); - Vector3 localDelta = Vector3::TransformNormal(worldDelta, worldToLocal); - - static auto lastDeltaLog = std::chrono::steady_clock::now(); - auto nowDelta = std::chrono::steady_clock::now(); - if (std::chrono::duration_cast(nowDelta - lastDeltaLog).count() > 500) { - logger::debug("VR Drag Delta - Local: ({:.3f}, {:.3f}, {:.3f})", localDelta.x, localDelta.y, localDelta.z); - lastDeltaLog = nowDelta; - } - - settings.VRMenuOffsetX = overlayDragState.initialHMDOffset.x + localDelta.x; - settings.VRMenuOffsetY = overlayDragState.initialHMDOffset.y + localDelta.y; - settings.VRMenuOffsetZ = overlayDragState.initialHMDOffset.z + localDelta.z; - settings.VRMenuScale = overlayDragState.initialHMDScale; - - static std::chrono::steady_clock::time_point lastLog = std::chrono::steady_clock::now(); - auto now = std::chrono::steady_clock::now(); - if (std::chrono::duration_cast(now - lastLog).count() > 500) { - logger::debug("VR Dragging (3D Mode): Offset ({:.2f}, {:.2f}, {:.2f}), Scale {:.2f}", - settings.VRMenuOffsetX, settings.VRMenuOffsetY, settings.VRMenuOffsetZ, settings.VRMenuScale); - lastLog = now; - } - } - break; - } - default: - break; - } - } - - // Joystick depth control during grip - if (overlayDragState.dragging) { - RE::VRControllerState* gripController = nullptr; - size_t thumbIdx = 0; - if (overlayDragState.isPrimary) { - if (lastKnownLeftHandedMode) { - gripController = &primaryControllerState; - thumbIdx = static_cast(RE::ControllerRole::Primary); - } else { - gripController = &secondaryControllerState; - thumbIdx = static_cast(RE::ControllerRole::Secondary); - } - } else if (overlayDragState.isSecondary) { - if (lastKnownLeftHandedMode) { - gripController = &secondaryControllerState; - thumbIdx = static_cast(RE::ControllerRole::Secondary); - } else { - gripController = &primaryControllerState; - thumbIdx = static_cast(RE::ControllerRole::Primary); - } - } - - if (gripController) { - float thumbY = gripController->thumbsticks[thumbIdx].y; - const float deadzone = settings.mouseDeadzone; - const float depthSpeed = 0.02f; - if (std::abs(thumbY) > deadzone) { - float depthDelta = -thumbY * depthSpeed; - if (overlayDragState.mode == OverlayDragState::DragMode::HMD) { - overlayDragState.initialHMDOffset.z += depthDelta; - overlayDragState.initialHMDOffset.z = std::clamp(overlayDragState.initialHMDOffset.z, -10.0f, 10.0f); - } else if (overlayDragState.mode == OverlayDragState::DragMode::Controller) { - overlayDragState.initialControllerOffset.z += depthDelta; - overlayDragState.initialControllerOffset.z = std::clamp(overlayDragState.initialControllerOffset.z, -10.0f, 10.0f); - } - } - } - } - - bool gripPressed = GetGripPressed(overlayDragState.isPrimary, overlayDragState.isSecondary); - if (!gripPressed) { - resetDragState(); - } -} - -void VR::TryStartNewDrag() -{ - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - auto* system = openvr ? openvr->vrSystem : nullptr; - if (!system) - return; - - struct DragMode - { - OverlayDragState::DragMode mode; - bool isActive; - std::function canStart; - std::function onInit; - }; - - std::vector dragModes; - - // Controller mode - only for opposite hand (highest priority) - if (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) { - dragModes.push_back({ OverlayDragState::DragMode::Controller, - true, - [&](vr::ETrackedControllerRole role) { - vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - if (attachedControllerIndex == vr::k_unTrackedDeviceIndexInvalid) - return false; - - ControllerDevice oppositeDevice = (settings.VRMenuAttachController == ControllerDevice::Primary) ? - ControllerDevice::Secondary : - ControllerDevice::Primary; - vr::TrackedDeviceIndex_t oppositeControllerIndex = Util::GetControllerIndexForDevice(oppositeDevice, lastKnownLeftHandedMode); - if (oppositeControllerIndex == vr::k_unTrackedDeviceIndexInvalid) - return false; - - for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { - if (system->GetTrackedDeviceClass(i) == vr::TrackedDeviceClass_Controller) { - vr::ETrackedControllerRole deviceRole = system->GetControllerRoleForTrackedDeviceIndex(i); - if (deviceRole == role && i == oppositeControllerIndex) - return true; - } - } - return false; - }, - [&]() { - overlayDragState.initialControllerOffset.x = settings.VRMenuControllerOffsetX; - overlayDragState.initialControllerOffset.y = settings.VRMenuControllerOffsetY; - overlayDragState.initialControllerOffset.z = settings.VRMenuControllerOffsetZ; - overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; - } }); - } - - // Fixed world mode - if (settings.VRMenuPositioningMethod == 1) { - std::function fixedWorldCanStart; - if (settings.attachMode == AttachMode::Both) { - fixedWorldCanStart = [&](vr::ETrackedControllerRole role) { - vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { - vr::ETrackedControllerRole actualAttachedRole = system->GetControllerRoleForTrackedDeviceIndex(attachedControllerIndex); - return role == actualAttachedRole; - } - return false; - }; - } else { - fixedWorldCanStart = CanStartAny; - } - - dragModes.push_back({ OverlayDragState::DragMode::FixedWorld, - true, - fixedWorldCanStart, - [&]() { - overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; - overlayDragState.initialOverlayMatrix = fixedWorldOverlayPosition.m; - } }); - } - - // HMD mode - if (settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) { - std::function hmdCanStart; - if (settings.attachMode == AttachMode::Both) { - hmdCanStart = [&](vr::ETrackedControllerRole role) { - vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { - vr::ETrackedControllerRole actualAttachedRole = system->GetControllerRoleForTrackedDeviceIndex(attachedControllerIndex); - return role == actualAttachedRole; - } - return false; - }; - } else { - hmdCanStart = CanStartAny; - } - - dragModes.push_back({ OverlayDragState::DragMode::HMD, - true, - hmdCanStart, - [&]() { - overlayDragState.initialHMDOffset.x = settings.VRMenuOffsetX; - overlayDragState.initialHMDOffset.y = settings.VRMenuOffsetY; - overlayDragState.initialHMDOffset.z = settings.VRMenuOffsetZ; - overlayDragState.initialHMDScale = settings.VRMenuScale; - overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; - } }); - } - - for (const auto& mode : dragModes) { - if (!mode.isActive) - continue; - for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { - if (system->GetTrackedDeviceClass(i) != vr::TrackedDeviceClass_Controller) - continue; - vr::ETrackedControllerRole role = system->GetControllerRoleForTrackedDeviceIndex(i); - bool isLeft = (role == vr::ETrackedControllerRole::TrackedControllerRole_LeftHand); - bool isRight = (role == vr::ETrackedControllerRole::TrackedControllerRole_RightHand); - if (!mode.canStart(role)) - continue; - bool gripPressed = GetGripPressed(isLeft, isRight); - if (!gripPressed) - continue; - float rawMatrix[3][4]; - if (!Util::GetControllerWorldMatrix(i, rawMatrix)) - continue; - vr::HmdMatrix34_t mat = Util::Float3x4ToHmdMatrix34(rawMatrix); - Matrix controllerMatrix = Util::HmdMatrix34ToMatrix(mat); - overlayDragState.dragging = true; - overlayDragState.mode = mode.mode; - overlayDragState.controllerIndex = i; - overlayDragState.isPrimary = isLeft; - overlayDragState.isSecondary = isRight; - overlayDragState.startControllerMatrix = controllerMatrix; - mode.onInit(); - - if (system && globals::menu->IsEnabled) { - for (vr::TrackedDeviceIndex_t deviceIdx = 0; deviceIdx < vr::k_unMaxTrackedDeviceCount; ++deviceIdx) { - if (system->GetTrackedDeviceClass(deviceIdx) == vr::TrackedDeviceClass_Controller) { - vr::ETrackedControllerRole deviceRole = system->GetControllerRoleForTrackedDeviceIndex(deviceIdx); - bool isRightController = (deviceRole == vr::ETrackedControllerRole::TrackedControllerRole_RightHand); - if (isRightController == isRight) { - openvr->TriggerHapticPulse(isRightController, 25.0f); - break; - } - } - } - } - - return; - } - } -} - -void VR::SetFixedOverlayToCurrentHMD() -{ - vr::HmdMatrix34_t transform = Util::ComputeOverlayTransformFromHMD( - settings.VRMenuOffsetX, - settings.VRMenuOffsetY, - settings.VRMenuOffsetZ); - fixedWorldOverlayPosition.m = Util::HmdMatrix34ToMatrix(transform); -} - -void VR::UpdateFixedWorldPositioning() -{ - if (settings.VRMenuPositioningMethod != 1) - return; - - if (!fixedWorldOverlayPosition.initialized) { - fixedWorldOverlayPosition.initialized = true; - SetFixedOverlayToCurrentHMD(); - auto player = RE::PlayerCharacter::GetSingleton(); - if (player) { - savedPlayerWorldPos = player->GetPosition(); - } - return; - } - - if (settings.VRMenuAutoResetDistance > 0.0f) { - auto player = RE::PlayerCharacter::GetSingleton(); - if (player) { - RE::NiPoint3 playerPos = player->GetPosition(); - float sqDist = playerPos.GetSquaredDistance(savedPlayerWorldPos); - float thresholdSq = settings.VRMenuAutoResetDistance * settings.VRMenuAutoResetDistance; - if (sqDist > thresholdSq) { - SetFixedOverlayToCurrentHMD(); - savedPlayerWorldPos = playerPos; - } - } - } -} diff --git a/src/Features/VR/SettingsUI.cpp b/src/Features/VR/SettingsUI.cpp deleted file mode 100644 index 33fd4aa76f..0000000000 --- a/src/Features/VR/SettingsUI.cpp +++ /dev/null @@ -1,1026 +0,0 @@ -#include "FeatureConstraints.h" -#include "Features/VR.h" -#include "Menu.h" -#include "Menu/Fonts.h" -#include "RE/B/BSOpenVR.h" -#include "RE/P/PlayerCharacter.h" -#include "Utils/PerfUtils.h" -#include "Utils/UI.h" -#include "Utils/VRUtils.h" - -#include - -using AttachMode = VR::Settings::OverlayAttachMode; - -namespace -{ - bool BeginTabItemWithFont(const char* label, Menu::FontRole role, ImGuiTabItemFlags flags = ImGuiTabItemFlags_None) - { - return MenuFonts::BeginTabItemWithFont(label, role, flags); - } -} - -//============================================================================= -// COMBO RECORDING HELPERS -//============================================================================= - -void VR::ResetComboRecording() -{ - isCapturingCombo = false; - currentComboType = ComboType::None; - currentComboName = nullptr; - recordedCombo.clear(); - comboStartTime = 0.0; - recordingButtonControllers.clear(); -} - -void VR::ApplyRecordedCombo() -{ - if (recordedCombo.empty()) - return; - - switch (currentComboType) { - case ComboType::MenuOpen: - settings.VRMenuOpenKeys = recordedCombo; - break; - case ComboType::MenuClose: - settings.VRMenuCloseKeys = recordedCombo; - break; - case ComboType::OverlayOpen: - settings.VROverlayOpenKeys = recordedCombo; - break; - case ComboType::OverlayClose: - settings.VROverlayCloseKeys = recordedCombo; - break; - default: - break; - } -} - -//============================================================================= -// OVERLAY (WELCOME MESSAGE) -//============================================================================= - -void VR::DrawOverlay() -{ - auto& vr = globals::features::vr; - if (!vr.IsOpenVRCompatible()) - return; - static LARGE_INTEGER overlayShowStart = { 0 }; - static LARGE_INTEGER freq = { 0 }; - - bool shouldShow = settings.kAutoHideSeconds > 0 && globals::game::ui && globals::game::ui->IsMenuOpen(RE::MainMenu::MENU_NAME) && globals::menu && !globals::menu->IsEnabled; - - if (!shouldShow) { - overlayShowStart.QuadPart = 0; - return; - } - - if (freq.QuadPart == 0) { - QueryPerformanceFrequency(&freq); - } - - LARGE_INTEGER now; - QueryPerformanceCounter(&now); - - if (overlayShowStart.QuadPart == 0) { - overlayShowStart = now; - } - - double elapsed = double(now.QuadPart - overlayShowStart.QuadPart) / double(freq.QuadPart); - const double autoHideSeconds = static_cast(settings.kAutoHideSeconds); - if (elapsed >= autoHideSeconds) { - return; - } - int secondsLeft = int(std::ceil(autoHideSeconds - elapsed)); - - ImGuiIO& io = ImGui::GetIO(); - ImVec2 overlaySize(520, 0); - ImVec2 overlayPos = ImVec2((io.DisplaySize.x - overlaySize.x) * 0.5f, (io.DisplaySize.y * 0.35f)); - ImGui::SetNextWindowPos(overlayPos, ImGuiCond_Always); - ImGui::SetNextWindowSize(overlaySize, ImGuiCond_Always); - ImGui::SetNextWindowBgAlpha(0.95f); - - ImGui::Begin("HowToUseOverlay", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav); - - ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 500.0f); - ImGui::TextWrapped("How to Use VR Community Shaders Menu:"); - ImGui::Separator(); - ImGui::TextWrapped("You must open the Main Menu or Tween Menu before VR controls work."); - ImGui::Spacing(); - ImGui::PopTextWrapPos(); - - ImGui::Text("Open Menu: "); - ImGui::SameLine(); - Util::DrawButtonCombo(settings.VRMenuOpenKeys, true); - - ImGui::Text("Close Menu: "); - ImGui::SameLine(); - Util::DrawButtonCombo(settings.VRMenuCloseKeys, true); - - ImGui::Spacing(); - ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 500.0f); - ImGui::TextWrapped("Grip + Thumbstick: Adjust overlay depth (closer/farther)"); - ImGui::Spacing(); - ImGui::TextWrapped("Tip: Disable this VR overlay by setting Attach Mode to 'None' in VR settings."); - ImGui::Spacing(); - ImGui::TextWrapped("(This welcome message will auto-hide in %d seconds)", secondsLeft); - ImGui::TextWrapped("(Disable in: VR settings > Controller Input Instructions)"); - ImGui::PopTextWrapPos(); - - ImGui::End(); -} - -//============================================================================= -// ANONYMOUS NAMESPACE: SETTINGS PANEL DRAW FUNCTIONS -//============================================================================= - -namespace -{ - void DrawControllerInputInstructions() - { - auto& vr = globals::features::vr; - auto& settings = vr.settings; - if (!vr.IsOpenVRCompatible()) - return; - if (ImGui::CollapsingHeader("Controller Input Instructions", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderInt("Auto-hide Welcome overlay timeout", &settings.kAutoHideSeconds, 0, VR::Config::kMaxAutoHideSeconds, - settings.kAutoHideSeconds <= 0 ? "Hidden" : "%d seconds"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Set to 0 to hide the overlay, or a positive value to show it for that many seconds"); - } - ImGui::TextWrapped("Menu (while in the main menu or tween menu):"); - if (ImGui::BeginTable("MenuInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Open Community Shaders Menu:"); - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(settings.VRMenuOpenKeys, true); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Close Community Shaders Menu:"); - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(settings.VRMenuCloseKeys, true); - ImGui::EndTable(); - } - ImGui::TextWrapped("Overlay (while in the main menu or tween menu):"); - if (ImGui::BeginTable("OverlayInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Open Overlay:"); - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(settings.VROverlayOpenKeys, true); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Close Overlay:"); - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(settings.VROverlayCloseKeys, true); - ImGui::EndTable(); - } - ImGui::TextWrapped("Menu Controller Input:"); - if (ImGui::BeginTable("ControllerInputTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "Trigger (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Left mouse button"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "Grip (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Right mouse button"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "Touchpad Click (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Middle mouse button"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "Stick Click (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Middle mouse button"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "A/X (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Enter"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerPrimaryColor(), "B/Y (Primary Controller)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Tab"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerSecondaryColor(), "B/Y (Secondary Controller)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Shift+Tab"); - ImGui::EndTable(); - } - bool useAttachedControllerForCursor = (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both); - if (ImGui::BeginTable("ThumbstickInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - if (useAttachedControllerForCursor) { - if (settings.VRMenuAttachController == ControllerDevice::Primary) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Mouse movement (attached controller)"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Scroll"); - } else { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Scroll"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Mouse movement (attached controller)"); - } - } else { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Mouse movement (HMD mode)"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Scroll"); - } - ImGui::EndTable(); - } - } - } - - void DrawGeneralVRSettings() - { - auto& vr = globals::features::vr; - VR::Settings& settings = vr.settings; - if (ImGui::CollapsingHeader("General Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - Util::ConstrainedUI::Checkbox("Enable Depth Buffer Culling in Exteriors", - &settings.EnableDepthBufferCullingExterior, - { "VR", "EnableDepthBufferCullingExterior" }); - auto exteriorConstraint = FeatureConstraints::GetConstraints({ "VR", "EnableDepthBufferCullingExterior" }); - if (!exteriorConstraint.isConstrained) { - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Improves performance in exteriors, recommended ON."); - } - } - - Util::ConstrainedUI::Checkbox("Enable Depth Buffer Culling in Interiors", - &settings.EnableDepthBufferCullingInterior, - { "VR", "EnableDepthBufferCullingInterior" }); - auto interiorConstraint = FeatureConstraints::GetConstraints({ "VR", "EnableDepthBufferCullingInterior" }); - if (!interiorConstraint.isConstrained) { - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Improves performance in interiors, recommended OFF due to occasional visual glitches."); - } - } - - if (ImGui::SliderFloat("Min Occludee Box Extent", &settings.MinOccludeeBoxExtent, 0.0f, 1000.0f, "%.1f")) - *vr.gMinOccludeeBoxExtent = settings.MinOccludeeBoxExtent; - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Minimum bounding box dimensions for object occlusion culling. Lower values improve performance but may result in visual artifacts."); - } - } - } - - void DrawMenuSettings() - { - auto& vr = globals::features::vr; - auto& settings = vr.settings; - if (!vr.IsOpenVRCompatible()) - return; - if (ImGui::CollapsingHeader("Menu Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - float maxScale = VR::Config::kMaxMenuScale; - ImGui::SliderFloat("Menu Scale", &settings.VRMenuScale, VR::Config::kMinMenuScale, maxScale, "%.2f"); - const char* positioningMethods[] = { "HMD Relative", "Fixed World Position" }; - int prevMethod = settings.VRMenuPositioningMethod; - if (ImGui::Combo("Menu Positioning Method", &settings.VRMenuPositioningMethod, positioningMethods, IM_ARRAYSIZE(positioningMethods))) { - if (prevMethod != 1 && settings.VRMenuPositioningMethod == 1) { - vr.SetFixedOverlayToCurrentHMD(); - auto player = RE::PlayerCharacter::GetSingleton(); - if (player) - vr.savedPlayerWorldPos = player->GetPosition(); - } - } - const char* attachModes[] = { "HMD Only", "Controller Only", "Both", "None (Disabled)" }; - int attachModeInt = static_cast(settings.attachMode); - if (ImGui::Combo("Attach Mode", &attachModeInt, attachModes, IM_ARRAYSIZE(attachModes))) { - settings.attachMode = static_cast(attachModeInt); - } - - if (settings.attachMode == AttachMode::ControllerOnly || - settings.attachMode == AttachMode::Both) { - const char* attachControllers[] = { "Primary Controller", "Secondary Controller" }; - int attachControllerInt = static_cast(settings.VRMenuAttachController); - if (ImGui::Combo("Attach to Controller", &attachControllerInt, attachControllers, IM_ARRAYSIZE(attachControllers))) { - settings.VRMenuAttachController = static_cast(attachControllerInt); - } - - ImGui::Separator(); - ImGui::Text("Controller Offset Settings"); - ImGui::SliderFloat("Controller Offset X", &settings.VRMenuControllerOffsetX, -2.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("Controller Offset Y", &settings.VRMenuControllerOffsetY, -2.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("Controller Offset Z", &settings.VRMenuControllerOffsetZ, -2.0f, 2.0f, "%.2f"); - } - - if (settings.attachMode == AttachMode::HMDOnly || - settings.attachMode == AttachMode::Both) { - ImGui::Separator(); - ImGui::Text("HMD Offset Settings"); - ImGui::SliderFloat("HMD Offset X", &settings.VRMenuOffsetX, -2.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("HMD Offset Y", &settings.VRMenuOffsetY, -2.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("HMD Offset Z", &settings.VRMenuOffsetZ, -4.0f, 1.0f, "%.2f"); - } - - if (settings.VRMenuPositioningMethod == 1) { - ImGui::Separator(); - ImGui::Text("Fixed World Position Settings"); - ImGui::SliderFloat("Auto Reset Distance (game units)", &settings.VRMenuAutoResetDistance, 100.0f, 5000.0f, "%.0f"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("If you move farther than this distance from the menu, it will automatically reset to your HMD position. %s", Util::Units::FormatDistance(settings.VRMenuAutoResetDistance).c_str()); - } - if (ImGui::Button("Reset Menu to HMD Position")) { - vr.SetFixedOverlayToCurrentHMD(); - } - } - } - } - - void DrawMouseSettings() - { - auto& vr = globals::features::vr; - if (!vr.IsOpenVRCompatible()) - return; - VR::Settings& settings = vr.settings; - if (ImGui::CollapsingHeader("Input Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::Checkbox("Enable Wand Pointing", &settings.EnableWandPointing)) { - vr.wandState.isIntersecting = false; - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Use controller ray-casting to point at UI elements"); - } - ImGui::Separator(); - ImGui::Text("Joystick Settings"); - ImGui::SliderFloat("Mouse Deadzone", &settings.mouseDeadzone, 0.0f, 1.0f, "%.2f"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Thumbstick deadzone for joystick cursor movement"); - } - ImGui::SliderFloat("Mouse Speed", &settings.mouseSpeed, 0.1f, 50.0f, "%.2f"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Speed multiplier for joystick cursor movement"); - } - } - } - - void DrawDragSettings() - { - auto& vr = globals::features::vr; - if (!vr.IsOpenVRCompatible()) - return; - VR::Settings& settings = vr.settings; - if (ImGui::CollapsingHeader("Drag Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::CollapsingHeader("Drag Instructions", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::TextWrapped("Overlay Positioning (Grip + Drag):"); - ImGui::BulletText("Fixed World Position: Any controller can drag (HMD-only mode) or attached controller only (Both modes)"); - ImGui::BulletText("HMD Relative: Any controller can drag (HMD-only mode) or attached controller only (Both modes)"); - ImGui::BulletText("Controller Attached: Only the opposite hand can drag the controller overlay"); - ImGui::Spacing(); - ImGui::TextWrapped("Depth Adjustment (Grip + Thumbstick):"); - ImGui::BulletText("While gripping to drag, use the thumbstick on the same hand to adjust depth"); - ImGui::BulletText("Thumbstick forward: Push overlay farther away"); - ImGui::BulletText("Thumbstick back: Pull overlay closer"); - } - ImGui::Checkbox("Enable drag to reposition overlays", &settings.EnableDragToReposition); - ImGui::BeginDisabled(!settings.EnableDragToReposition); - ImGui::ColorEdit4("Drag Highlight Color", settings.dragHighlightColor.data()); - ImGui::EndDisabled(); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Color used to highlight draggable overlays in VR."); - } - } - } - - void DrawKeyBindings() - { - auto& vr = globals::features::vr; - auto& settings = vr.settings; - - if (ImGui::CollapsingHeader("Combo Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("Combo Timeout", &settings.comboTimeout, 1.0f, 10.0f, "%.1f seconds"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Time limit for recording button combinations."); - } - } - ImGui::Separator(); - const char* comboTypes[] = { - "Open Community Shaders Menu", - "Close Community Shaders Menu", - "Open VR Overlay", - "Close VR Overlay" - }; - static int selectedComboIndex = 0; - ImGui::Text("Select Combo to Record:"); - ImGui::SameLine(); - if (ImGui::Combo("##ComboSelector", &selectedComboIndex, comboTypes, IM_ARRAYSIZE(comboTypes))) { - vr.isCapturingCombo = false; - vr.currentComboType = VR::ComboType::None; - vr.recordedCombo.clear(); - } - if (ImGui::Button("Record Selected Combo")) { - vr.isCapturingCombo = true; - vr.currentComboType = static_cast(selectedComboIndex + 1); - vr.currentComboName = comboTypes[selectedComboIndex]; - vr.recordedCombo.clear(); - vr.comboStartTime = Util::GetNowSecs(); - vr.recordingButtonControllers.clear(); - } - ImGui::SameLine(); - if (ImGui::SmallButton("Clear")) { - switch (selectedComboIndex) { - case 0: - settings.VRMenuOpenKeys.clear(); - break; - case 1: - settings.VRMenuCloseKeys.clear(); - break; - case 2: - settings.VROverlayOpenKeys.clear(); - break; - case 3: - settings.VROverlayCloseKeys.clear(); - break; - } - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Click to start recording a new button combination for the selected action."); - } - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - if (ImGui::BeginTable("##VRBindingsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("Action"); - ImGui::TableSetupColumn("Current Binding"); - ImGui::TableSetupColumn("Description"); - ImGui::TableHeadersRow(); - struct VRKeyBindingConfig - { - const char* label; - std::vector& combos; - const char* description; - const char* controllerRequirement; - }; - std::vector keyBindingConfigs = { - { "Open Community Shaders Menu", settings.VRMenuOpenKeys, "Button combination to open the Community Shaders menu", "Primary" }, - { "Close Community Shaders Menu", settings.VRMenuCloseKeys, "Button combination to close the Community Shaders menu", "Both" }, - { "Open VR Overlay", settings.VROverlayOpenKeys, "Button combination to open the VR overlay", "Primary" }, - { "Close VR Overlay", settings.VROverlayCloseKeys, "Button combination to close the VR overlay", "Secondary" } - }; - for (size_t row = 0; row < keyBindingConfigs.size(); ++row) { - const auto& config = keyBindingConfigs[row]; - ImGui::TableNextRow(); - if (row == static_cast(selectedComboIndex)) { - ImU32 highlight = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 0.0f, 0.15f)); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, highlight); - } - ImGui::TableSetColumnIndex(0); - char selectableId[64]; - snprintf(selectableId, sizeof(selectableId), "##combo_row_%zu", row); - bool rowSelected = (row == static_cast(selectedComboIndex)); - if (ImGui::Selectable(selectableId, rowSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowItemOverlap, ImVec2(0, 0))) { - selectedComboIndex = static_cast(row); - } - ImGui::SameLine(0, 0); - ImGui::Text("%s", config.label); - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(config.combos, false); - ImGui::TableSetColumnIndex(2); - ImGui::Text("%s", config.description); - } - ImGui::EndTable(); - } - ImGui::Spacing(); - if (ImGui::Button("Reset to Defaults")) { - VR::Settings defaults; - settings.VRMenuOpenKeys = defaults.VRMenuOpenKeys; - settings.VRMenuCloseKeys = defaults.VRMenuCloseKeys; - settings.VROverlayOpenKeys = defaults.VROverlayOpenKeys; - settings.VROverlayCloseKeys = defaults.VROverlayCloseKeys; - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Reset all VR key bindings to their default values."); - } - } - - void DrawThumbstickColumn(VR& vr, bool showPrimary, ImU32 highlightCol) - { - auto& state = showPrimary ? vr.primaryControllerState : vr.secondaryControllerState; - auto role = showPrimary ? RE::ControllerRole::Primary : RE::ControllerRole::Secondary; - float x = state.thumbsticks[static_cast(role)].x; - float y = state.thumbsticks[static_cast(role)].y; - - ImVec2 padSize = ImVec2(80, 80); - ImVec2 cursor = ImGui::GetCursorScreenPos(); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - ImVec2 center = ImVec2(cursor.x + padSize.x / 2, cursor.y + padSize.y / 2); - float radius = padSize.x / 2 - 4; - ImU32 borderCol = ImGui::GetColorU32(ImGuiCol_Border); - ImU32 axisCol = ImGui::GetColorU32(ImGuiCol_TextDisabled); - ImU32 dotCol = ImGui::GetColorU32(ImGuiCol_Text); - - drawList->AddRectFilled(cursor, ImVec2(cursor.x + padSize.x, cursor.y + padSize.y), ImGui::GetColorU32(ImGuiCol_FrameBg)); - drawList->AddRect(cursor, ImVec2(cursor.x + padSize.x, cursor.y + padSize.y), borderCol, 4.0f, 0, 2.0f); - drawList->AddLine(ImVec2(center.x, cursor.y + 4), ImVec2(center.x, cursor.y + padSize.y - 4), axisCol, 1.0f); - drawList->AddLine(ImVec2(cursor.x + 4, center.y), ImVec2(cursor.x + padSize.x - 4, center.y), axisCol, 1.0f); - - int quad = 0; - if (x > 0 && y > 0) - quad = 1; - else if (x < 0 && y > 0) - quad = 2; - else if (x < 0 && y < 0) - quad = 3; - else if (x > 0 && y < 0) - quad = 4; - - if (quad != 0) { - ImVec2 q0 = center, q1 = center, q2 = center, q3 = center; - if (quad == 1) { - q1 = { center.x + radius, center.y - radius }; - q2 = { center.x + radius, center.y }; - q3 = { center.x, center.y - radius }; - } else if (quad == 2) { - q1 = { center.x - radius, center.y - radius }; - q2 = { center.x - radius, center.y }; - q3 = { center.x, center.y - radius }; - } else if (quad == 3) { - q1 = { center.x - radius, center.y + radius }; - q2 = { center.x - radius, center.y }; - q3 = { center.x, center.y + radius }; - } else if (quad == 4) { - q1 = { center.x + radius, center.y + radius }; - q2 = { center.x + radius, center.y }; - q3 = { center.x, center.y + radius }; - } - ImVec2 poly[4] = { center, q1, q2, q3 }; - drawList->AddConvexPolyFilled(poly, 4, highlightCol); - } - - ImVec2 dot = ImVec2(center.x + x * radius, center.y - y * radius); - drawList->AddCircleFilled(dot, 5.0f, dotCol); - - ImGui::Dummy(padSize); - ImGui::SetNextItemWidth(160.0f); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetTextLineHeight()); - ImGui::Text("X: %+1.3f Y: %+1.3f [%s]", x, y, RE::GetQuadrantName(x, y)); - } - - void DrawDebugSection() - { - auto& vr = globals::features::vr; - auto& settings = vr.settings; - auto menu = globals::menu; - - if (ImGui::CollapsingHeader("OpenVR Information", ImGuiTreeNodeFlags_DefaultOpen)) { - auto& info = vr.openVRInfo; - if (info.isAvailable) { - if (vr.IsOpenVRCompatible()) { - ImGui::Text("OpenVR System: Active & Compatible"); - } else { - ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "OpenVR System: Active but INCOMPATIBLE"); - ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "VR overlay menus disabled."); - } - - ImGui::Text("Runtime: %s", VRDetection::RuntimeTypeToString(info.runtimeType)); - ImGui::Text("DLL Path: %s", info.dllPath.c_str()); - ImGui::Text("DLL Version: %s", info.version.c_str()); - ImGui::Text("DLL Size: %llu bytes", info.fileSize); - ImGui::Text("Modified: %s", info.modificationTime.c_str()); - - ImGui::Separator(); - ImGui::Text("Detection Method:"); - ImGui::Text(" Interface Probing: %s", info.probingSucceeded ? "Passed" : "Failed"); - ImGui::Text(" IVROverlay_016: %s", info.hasOverlayInterface ? "OK" : "Missing"); - ImGui::Text(" IVRSystem_017: %s", info.hasSystemInterface ? "OK" : "Missing"); - ImGui::Text(" IVRCompositor_021: %s", info.hasCompositorInterface ? "OK" : "Missing"); - ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), " Rendering: In-scene overlay (submit hook)"); - - } else { - ImGui::Text("OpenVR system not available"); - } - } - - if (ImGui::CollapsingHeader("Controller Diagnostics", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::Checkbox("Test Mode: Disable controller menu input (except scroll controller and triggers)", &settings.VRMenuControllerDiagnosticsTestMode)) { - ImGui::SetScrollHereY(0.0f); - } - ImGui::SeparatorText("Button State"); - double nowSecs = Util::GetNowSecs(); - ImVec4 highlightColor = menu->GetTheme().StatusPalette.InfoColor; - ImU32 highlightColorU32 = ImGui::ColorConvertFloat4ToU32(highlightColor); - - bool isLeftHanded = vr.lastKnownLeftHandedMode; - - if (ImGui::BeginTable("vr_input_state_table", 7, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableSetupColumn("Button"); - if (isLeftHanded) { - ImGui::TableSetupColumn("Primary State"); - ImGui::TableSetupColumn("Primary Held (s)"); - ImGui::TableSetupColumn("Primary Type"); - ImGui::TableSetupColumn("Secondary State"); - ImGui::TableSetupColumn("Secondary Held (s)"); - ImGui::TableSetupColumn("Secondary Type"); - } else { - ImGui::TableSetupColumn("Secondary State"); - ImGui::TableSetupColumn("Secondary Held (s)"); - ImGui::TableSetupColumn("Secondary Type"); - ImGui::TableSetupColumn("Primary State"); - ImGui::TableSetupColumn("Primary Held (s)"); - ImGui::TableSetupColumn("Primary Type"); - } - ImGui::TableHeadersRow(); - - auto DrawButtonType = [](const RE::ButtonState& state) { - if (!state.isPressed) { - if (state.IsClick()) - ImGui::TextUnformatted("Click"); - else if (state.IsHold()) - ImGui::TextUnformatted("Hold"); - else - ImGui::TextUnformatted("-"); - } else { - ImGui::TextUnformatted("Held"); - } - }; - - auto printRow = [&](const char* label, const RE::ButtonState& left, const RE::ButtonState& right) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(label); - ImGui::TableSetColumnIndex(1); - if (left.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - ImGui::TextUnformatted(left.isPressed ? "Pressed" : "Released"); - ImGui::TableSetColumnIndex(2); - if (left.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - ImGui::Text("%.2f", left.GetCurrentHeldTime(nowSecs)); - ImGui::TableSetColumnIndex(3); - if (left.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - DrawButtonType(left); - ImGui::TableSetColumnIndex(4); - if (right.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - ImGui::TextUnformatted(right.isPressed ? "Pressed" : "Released"); - ImGui::TableSetColumnIndex(5); - if (right.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - ImGui::Text("%.2f", right.GetCurrentHeldTime(nowSecs)); - ImGui::TableSetColumnIndex(6); - if (right.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - DrawButtonType(right); - }; - - auto printRowWithHandedness = [&](const char* label, auto key) { - auto& primary = vr.primaryControllerState[key]; - auto& secondary = vr.secondaryControllerState[key]; - if (isLeftHanded) { - printRow(label, primary, secondary); - } else { - printRow(label, secondary, primary); - } - }; - - printRowWithHandedness("Trigger", RE::BSOpenVRControllerDevice::Keys::kTrigger); - printRowWithHandedness("Grip", RE::BSOpenVRControllerDevice::Keys::kGrip); - printRowWithHandedness("GripAlt", RE::BSOpenVRControllerDevice::Keys::kGripAlt); - printRowWithHandedness("Stick Click", RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger); - printRowWithHandedness("Touchpad Click", RE::BSOpenVRControllerDevice::Keys::kTouchpadClick); - printRowWithHandedness("Touchpad Alt", RE::BSOpenVRControllerDevice::Keys::kTouchpadAlt); - printRowWithHandedness("B/Y", RE::BSOpenVRControllerDevice::Keys::kBY); - printRowWithHandedness("A/X", RE::BSOpenVRControllerDevice::Keys::kXA); - ImGui::EndTable(); - } - - ImGui::SeparatorText("VR Thumbstick State"); - ImU32 highlightCol = ImGui::ColorConvertFloat4ToU32(menu->GetTheme().StatusPalette.InfoColor); - if (ImGui::BeginTable("##VRThumbstickTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) { - if (isLeftHanded) { - ImGui::TableSetupColumn("Primary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); - ImGui::TableSetupColumn("Secondary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); - } else { - ImGui::TableSetupColumn("Secondary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); - ImGui::TableSetupColumn("Primary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); - } - ImGui::TableHeadersRow(); - - // Left column - ImGui::TableSetColumnIndex(0); - ImGui::BeginGroup(); - DrawThumbstickColumn(vr, isLeftHanded, highlightCol); - ImGui::EndGroup(); - - // Right column - ImGui::TableSetColumnIndex(1); - ImGui::BeginGroup(); - DrawThumbstickColumn(vr, !isLeftHanded, highlightCol); - ImGui::EndGroup(); - ImGui::EndTable(); - } - - ImGui::SeparatorText("Recent VR Controller Events"); - ImGui::TextDisabled("Note: For thumbstick events, KeyCode/Value columns show X/Y floats."); - if (ImGui::BeginTable("eventlog", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) { - ImGui::TableSetupColumn("Device", ImGuiTableColumnFlags_WidthFixed, 60.0f); - ImGui::TableSetupColumn("KeyCode/X", ImGuiTableColumnFlags_WidthFixed, 80.0f); - ImGui::TableSetupColumn("Value/Y", ImGuiTableColumnFlags_WidthFixed, 80.0f); - ImGui::TableSetupColumn("Pressed", ImGuiTableColumnFlags_WidthFixed, 70.0f); - ImGui::TableSetupColumn("Known Mapping", ImGuiTableColumnFlags_WidthFixed, 120.0f); - ImGui::TableSetupColumn("Event Type", ImGuiTableColumnFlags_WidthFixed, 120.0f); - ImGui::TableHeadersRow(); - for (const auto& e : vr.vrControllerEventLog) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("%d", e.device); - ImGui::TableSetColumnIndex(1); - if (e.heldSource == "thumbstick") { - ImGui::Text("%.3f", e.thumbstickX); - } else { - ImGui::Text("%d", e.keyCode); - } - ImGui::TableSetColumnIndex(2); - if (e.heldSource == "thumbstick") { - ImGui::Text("%.3f", e.thumbstickY); - } else { - ImGui::Text("%d", e.value); - } - ImGui::TableSetColumnIndex(3); - ImGui::Text("%s", e.pressed ? "Pressed" : "Released"); - ImGui::TableSetColumnIndex(4); - if (e.heldSource == "thumbstick") { - ImGui::TextUnformatted(e.controllerRole.c_str()); - } else { - ImGui::TextUnformatted(RE::GetOpenVRButtonName(e.keyCode)); - } - ImGui::TableSetColumnIndex(5); - if (e.heldSource == "thumbstick") { - ImGui::TextUnformatted("-"); - } else { - if (!e.pressed) { - if (e.heldTime > 0.0) { - if (e.heldTime < 0.5) { - ImGui::Text("Click (%.2fs)", e.heldTime); - } else { - ImGui::Text("Hold (%.2fs)", e.heldTime); - } - } else { - ImGui::Text("Release"); - } - } else if (e.pressed) { - if (e.heldTime > 0.0) { - ImGui::Text("Held for %.2fs", e.heldTime); - } else { - ImGui::Text("Press"); - } - } - } - } - ImGui::EndTable(); - } - - ImGui::SeparatorText("Wand Pointing State"); - if (ImGui::BeginTable("##WandPointingState", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableSetupColumn("Property", ImGuiTableColumnFlags_WidthFixed, 200.0f); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableHeadersRow(); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Wand Pointing Enabled"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("%s", settings.EnableWandPointing ? "Yes" : "No"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Intersecting Overlay"); - ImGui::TableSetColumnIndex(1); - if (vr.wandState.isIntersecting) { - ImGui::TextColored(menu->GetTheme().StatusPalette.InfoColor, "YES"); - } else { - ImGui::Text("No"); - } - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("UV Coordinates"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("(%.3f, %.3f)", vr.wandState.uvCoordinates.x, vr.wandState.uvCoordinates.y); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Controller Index"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("%u", vr.wandState.controllerIndex); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Ray Origin"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("(%.2f, %.2f, %.2f)", vr.wandState.rayOrigin.x, vr.wandState.rayOrigin.y, vr.wandState.rayOrigin.z); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Ray Direction"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("(%.2f, %.2f, %.2f)", vr.wandState.rayDirection.x, vr.wandState.rayDirection.y, vr.wandState.rayDirection.z); - - ImGui::EndTable(); - } - } - - if (ImGui::CollapsingHeader("OpenVR Addresses")) { - auto openvr = RE::BSOpenVR::GetSingleton(); - auto overlay = openvr ? RE::BSOpenVR::GetIVROverlayFromContext(&openvr->vrContext) : nullptr; - auto vrSystem = openvr ? openvr->vrSystem : nullptr; - ADDRESS_NODE(openvr) - ADDRESS_NODE(overlay) - ADDRESS_NODE(vrSystem) - } - } -} // namespace - -//============================================================================= -// DRAW SETTINGS (main entry point) -//============================================================================= - -void VR::DrawSettings() -{ - auto menu = globals::menu; - if (!menu) - return; - if (ImGui::BeginTabBar("##VRTabs", ImGuiTabBarFlags_None)) { - if (BeginTabItemWithFont("General", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##VRGeneralFrame", { 0, 0 }, true)) { - DrawGeneralVRSettings(); - DrawControllerInputInstructions(); - DrawMenuSettings(); - DrawMouseSettings(); - DrawDragSettings(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - if (IsOpenVRCompatible()) { - if (BeginTabItemWithFont("Bindings", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##VRBindingsFrame", { 0, 0 }, true)) { - DrawKeyBindings(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - } - - if (BeginTabItemWithFont("Debug", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##VRDebugFrame", { 0, 0 }, true)) { - DrawDebugSection(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - ImGui::EndTabBar(); - } - - // Combo recording popup - if (this->isCapturingCombo) { - ImGui::OpenPopup("Record Combo"); - ImGui::SetNextWindowSize(ImVec2(400, 200), ImGuiCond_FirstUseEver); - if (ImGui::BeginPopupModal("Record Combo", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - auto GetButtonName = [](uint32_t key) -> const char* { - switch (key) { - case static_cast(RE::BSOpenVRControllerDevice::Keys::kTrigger): - return "Trigger"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kGrip): - return "Grip"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kTouchpadClick): - return "Touchpad"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger): - return "Stick Click"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kXA): - return "A/X"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kBY): - return "B/Y"; - default: - return "Unknown"; - } - }; - - ImGui::Text("Recording combo for: %s", this->currentComboName ? this->currentComboName : "Unknown"); - ImGui::Spacing(); - - ImGui::TextDisabled("(During recording, any controller's buttons can be used. Requirement is only enforced during use.)"); - - ImGui::Spacing(); - - double remainingTime = settings.comboTimeout - (Util::GetNowSecs() - this->comboStartTime); - ImVec4 timerColor = remainingTime > 2.0 ? Util::Colors::GetTimerGood() : - remainingTime > 1.0 ? Util::Colors::GetTimerWarning() : - Util::Colors::GetTimerCritical(); - ImGui::TextColored(timerColor, "Time remaining: %.1f seconds", remainingTime); - - ImGui::Spacing(); - - if (this->recordedCombo.empty()) { - ImGui::Text("Press buttons to record combo..."); - } else { - ImGui::Text("Recorded buttons:"); - std::vector sortedRecordedCombos; - for (size_t i = 0; i < this->recordedCombo.size(); ++i) { - sortedRecordedCombos.push_back(this->recordedCombo[i]); - } - std::sort(sortedRecordedCombos.begin(), sortedRecordedCombos.end(), - [](const ButtonCombo& a, const ButtonCombo& b) { - return a.GetKey() < b.GetKey(); - }); - - Util::DrawButtonCombo(sortedRecordedCombos, false); - } - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - ImGui::Text("Press ENTER to accept, ESC to cancel"); - - // Handle button recording - bool buttonPressed = false; - uint32_t pressedKey = 0; - ControllerDevice pressedDevice = ControllerDevice::Both; - - for (const auto& [keyCode, buttonState] : primaryControllerState.GetActiveButtons()) { - if (buttonState->isPressed) { - pressedKey = keyCode; - buttonPressed = true; - pressedDevice = ControllerDevice::Primary; - break; - } - } - - if (!buttonPressed) { - for (const auto& [keyCode, buttonState] : secondaryControllerState.GetActiveButtons()) { - if (buttonState->isPressed) { - pressedKey = keyCode; - buttonPressed = true; - pressedDevice = ControllerDevice::Secondary; - break; - } - } - } - - if (buttonPressed) { - auto it = recordingButtonControllers.find(pressedKey); - if (it == recordingButtonControllers.end()) { - recordingButtonControllers[pressedKey] = pressedDevice; - } else { - if (it->second != pressedDevice && it->second != ControllerDevice::Both) { - it->second = ControllerDevice::Both; - } - } - this->recordedCombo.clear(); - for (const auto& [key, device] : recordingButtonControllers) { - this->recordedCombo.push_back(ButtonCombo(device, key)); - } - } - - if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter)) || ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_KeypadEnter))) { - ApplyRecordedCombo(); - ResetComboRecording(); - ImGui::CloseCurrentPopup(); - } - - if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape))) { - ResetComboRecording(); - ImGui::CloseCurrentPopup(); - } - - if (remainingTime <= 0.0) { - ApplyRecordedCombo(); - ResetComboRecording(); - ImGui::CloseCurrentPopup(); - } - - ImGui::EndPopup(); - } - } -} diff --git a/src/Features/VR/WandPointing.cpp b/src/Features/VR/WandPointing.cpp deleted file mode 100644 index 5a76019721..0000000000 --- a/src/Features/VR/WandPointing.cpp +++ /dev/null @@ -1,144 +0,0 @@ -#include "Features/VR.h" -#include "RE/B/BSOpenVR.h" -#include "Utils/VRUtils.h" - -#include -#include -#include - -using namespace DirectX::SimpleMath; -using AttachMode = VR::Settings::OverlayAttachMode; - -bool VR::ComputeWandIntersectionForOverlayType(OverlayType type, vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV) -{ - float controllerM[3][4]; - if (!Util::GetControllerWorldMatrix(controllerIndex, controllerM)) { - return false; - } - Matrix controllerWorld = Util::HmdMatrix34ToMatrix(Util::Float3x4ToHmdMatrix34(controllerM)); - Vector3 rayOrigin = controllerWorld.Translation(); - Vector3 rayDir = controllerWorld.Forward(); - - // Update debug state - wandState.rayOrigin = rayOrigin; - wandState.rayDirection = rayDir; - Matrix overlayWorld; - if (type == OverlayType::HMD) { - if (settings.VRMenuPositioningMethod == 1) { // Fixed World - overlayWorld = fixedWorldOverlayPosition.m; - } else { // HMD Relative - vr::TrackedDevicePose_t hmdPose; - if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &hmdPose, 1)) - return false; - if (!hmdPose.bPoseIsValid) - return false; - Matrix hmdWorld = Util::HmdMatrix34ToMatrix(hmdPose.mDeviceToAbsoluteTracking); - Matrix offset = Matrix::CreateTranslation(settings.VRMenuOffsetX, settings.VRMenuOffsetY, settings.VRMenuOffsetZ); - overlayWorld = offset * hmdWorld; - } - } else { // Controller Relative - vr::TrackedDeviceIndex_t attachIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - if (attachIndex == vr::k_unTrackedDeviceIndexInvalid) - return false; - - float attachM[3][4]; - if (!Util::GetControllerWorldMatrix(attachIndex, attachM)) - return false; - Matrix attachWorld = Util::HmdMatrix34ToMatrix(Util::Float3x4ToHmdMatrix34(attachM)); - - Matrix offset = Matrix::CreateTranslation(settings.VRMenuControllerOffsetX, settings.VRMenuControllerOffsetY, settings.VRMenuControllerOffsetZ); - overlayWorld = offset * attachWorld; - } - - if (settings.VRMenuScale < 1e-4f) - return false; - overlayWorld = Matrix::CreateScale(settings.VRMenuScale) * overlayWorld; - - Matrix worldToOverlay = overlayWorld.Invert(); - Vector3 localOrigin = Vector3::Transform(rayOrigin, worldToOverlay); - Vector3 localDir = Vector3::TransformNormal(rayDir, worldToOverlay); - - if (std::abs(localDir.z) < 1e-6f) - return false; - - float t = -localOrigin.z / localDir.z; - if (t < 0.0f) - return false; - - Vector3 hit = localOrigin + t * localDir; - - if (hit.x < -0.5f || hit.x > 0.5f || hit.y < -0.5f || hit.y > 0.5f) - return false; - - outUV.x = hit.x + 0.5f; - outUV.y = 0.5f - hit.y; - - return true; -} - -bool VR::ComputeWandIntersection(vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV) -{ - bool intersected = false; - if (settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) { - if (ComputeWandIntersectionForOverlayType(OverlayType::HMD, controllerIndex, outUV)) { - intersected = true; - } - } - if (!intersected && (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both)) { - if (ComputeWandIntersectionForOverlayType(OverlayType::Controller, controllerIndex, outUV)) { - intersected = true; - } - } - - if (intersected) { - wandState.isIntersecting = true; - wandState.uvCoordinates = outUV; - wandState.controllerIndex = controllerIndex; - } else { - wandState.isIntersecting = false; - } - - return intersected; -} - -void VR::UpdateCursorFromWandPointing() -{ - if (!settings.EnableWandPointing || !globals::menu || !globals::menu->IsEnabled) - return; - - ImGuiIO& io = ImGui::GetIO(); - - vr::TrackedDeviceIndex_t pointingController = vr::k_unTrackedDeviceIndexInvalid; - - if (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) { - ControllerDevice oppositeController = (settings.VRMenuAttachController == ControllerDevice::Primary) ? - ControllerDevice::Secondary : - ControllerDevice::Primary; - pointingController = Util::GetControllerIndexForDevice(oppositeController, lastKnownLeftHandedMode); - } else { - pointingController = Util::GetControllerIndexForDevice(ControllerDevice::Primary, lastKnownLeftHandedMode); - } - - if (pointingController == vr::k_unTrackedDeviceIndexInvalid) { - wandState.isIntersecting = false; - return; - } - - ImVec2 uv; - bool intersected = ComputeWandIntersection(pointingController, uv); - - if (intersected) { - float screenX = uv.x * io.DisplaySize.x; - float screenY = uv.y * io.DisplaySize.y; - - screenX = std::clamp(screenX, 0.0f, io.DisplaySize.x); - screenY = std::clamp(screenY, 0.0f, io.DisplaySize.y); - - io.MousePos = ImVec2(screenX, screenY); - io.AddMousePosEvent(screenX, screenY); - io.MouseDrawCursor = true; - io.WantSetMousePos = true; - } else { - wandState.isIntersecting = false; - } -} diff --git a/src/Menu.cpp b/src/Menu.cpp index f9dd217831..a6437abc17 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -232,6 +232,8 @@ Menu::~Menu() ImGui_ImplWin32_Shutdown(); ImGui::DestroyContext(); dxgiAdapter3 = nullptr; + + globals::features::vr.DestroyOverlay(); } void Menu::Load(json& o_json) @@ -649,6 +651,10 @@ void Menu::Init() BuildCategoryCounts(); + if (globals::features::vr.IsOpenVRCompatible()) { + globals::features::vr.EnsureOverlayInitialized(); + } + initialized = true; } diff --git a/src/Utils/VRUtils.cpp b/src/Utils/VRUtils.cpp index 23ac293156..28daf4c586 100644 --- a/src/Utils/VRUtils.cpp +++ b/src/Utils/VRUtils.cpp @@ -105,6 +105,35 @@ namespace Util return transform; } + vr::HmdMatrix34_t CreateControllerOverlayTransform(float offsetX, float offsetY, float offsetZ, float width, float height) + { + vr::HmdMatrix34_t transform; + transform.m[0][0] = width; + transform.m[0][1] = 0.0f; + transform.m[0][2] = 0.0f; + transform.m[0][3] = offsetX; + transform.m[1][0] = 0.0f; + transform.m[1][1] = height; + transform.m[1][2] = 0.0f; + transform.m[1][3] = offsetY; + transform.m[2][0] = 0.0f; + transform.m[2][1] = 0.0f; + transform.m[2][2] = 1.0f; + transform.m[2][3] = offsetZ; + return transform; + } + + void SetOverlayInputFlags(vr::IVROverlay* overlay, vr::VROverlayHandle_t handle) + { + if (!overlay || handle == vr::k_ulOverlayHandleInvalid) + return; + + overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRScrollEvents, true); + overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRTouchpadEvents, true); + overlay->SetOverlayFlag(handle, vr::VROverlayFlags_AcceptsGamepadEvents, true); + overlay->SetOverlayFlag(handle, vr::VROverlayFlags_VisibleInDashboard, true); + } + //============================================================================= // NEW ACTIVE FUNCTIONS FROM VR.CPP //============================================================================= @@ -124,6 +153,30 @@ namespace Util } } + ImVec4 GetControllerDeviceColor(InputDeviceType device, bool isRecording) + { + // UI color constants from VR.cpp + constexpr ImVec4 Primary = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); // Green + constexpr ImVec4 Secondary = ImVec4(0.0f, 0.6f, 1.0f, 1.0f); // Blue + constexpr ImVec4 Both = ImVec4(0.5f, 0.0f, 0.5f, 1.0f); // Purple + constexpr ImVec4 Recording = ImVec4(1.0f, 0.65f, 0.0f, 1.0f); // Orange + constexpr ImVec4 Default = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White + + if (isRecording && device == InputDeviceType::Both) { + return Recording; // Orange for recording mode + } + switch (device) { + case InputDeviceType::Primary: + return Primary; + case InputDeviceType::Secondary: + return Secondary; + case InputDeviceType::Both: + return Both; + default: + return Default; + } + } + vr::TrackedDeviceIndex_t GetControllerIndexForDevice(InputDeviceType device, bool isLeftHanded) { OpenVRContext ctx; @@ -238,4 +291,48 @@ namespace Util return false; } + //============================================================================= + // WAND POINTING IMPLEMENTATION + //============================================================================= + + bool ComputeWandIntersection(vr::IVROverlay* overlay, vr::VROverlayHandle_t overlayHandle, + vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV) + { + if (!overlay || overlayHandle == vr::k_ulOverlayHandleInvalid || controllerIndex == vr::k_unTrackedDeviceIndexInvalid) + return false; + + // Bounds check to prevent array out-of-bounds access + if (controllerIndex >= vr::k_unMaxTrackedDeviceCount) + return false; + + // Get controller pose + vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount]; + if (!GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, poses, vr::k_unMaxTrackedDeviceCount)) + return false; + + if (!poses[controllerIndex].bPoseIsValid) + return false; + + // Compute intersection using OpenVR's built-in ray-casting + vr::VROverlayIntersectionParams_t params; + params.eOrigin = vr::TrackingUniverseStanding; + params.vSource.v[0] = poses[controllerIndex].mDeviceToAbsoluteTracking.m[0][3]; + params.vSource.v[1] = poses[controllerIndex].mDeviceToAbsoluteTracking.m[1][3]; + params.vSource.v[2] = poses[controllerIndex].mDeviceToAbsoluteTracking.m[2][3]; + + // Ray direction is the -Z axis of the controller (forward vector) + params.vDirection.v[0] = -poses[controllerIndex].mDeviceToAbsoluteTracking.m[0][2]; + params.vDirection.v[1] = -poses[controllerIndex].mDeviceToAbsoluteTracking.m[1][2]; + params.vDirection.v[2] = -poses[controllerIndex].mDeviceToAbsoluteTracking.m[2][2]; + + vr::VROverlayIntersectionResults_t results; + if (overlay->ComputeOverlayIntersection(overlayHandle, ¶ms, &results)) { + // Convert UV coordinates (0-1 range) to output + outUV.x = results.vUVs.v[0]; + outUV.y = results.vUVs.v[1]; + return true; + } + + return false; + } } \ No newline at end of file diff --git a/src/Utils/VRUtils.h b/src/Utils/VRUtils.h index 0e52b283d6..ef4786fb62 100644 --- a/src/Utils/VRUtils.h +++ b/src/Utils/VRUtils.h @@ -54,6 +54,19 @@ namespace Util */ void DrawButtonCombo(const std::vector& combo, bool showControllerLabels); + /** + * @brief Sets standard input flags for a VR overlay + * @param overlay Pointer to the OpenVR overlay interface + * @param handle Handle to the VR overlay + * + * This function configures an overlay to accept VR input events including: + * - Scroll events + * - Touchpad events + * - Gamepad events + * - Dashboard visibility + */ + void SetOverlayInputFlags(vr::IVROverlay* overlay, vr::VROverlayHandle_t handle); + /** * @brief Computes a transformation matrix for positioning an overlay relative to the HMD * @param offsetX Horizontal offset from HMD in meters (positive = right) @@ -66,6 +79,20 @@ namespace Util */ vr::HmdMatrix34_t ComputeOverlayTransformFromHMD(float offsetX, float offsetY, float offsetZ); + /** + * @brief Creates a transformation matrix for controller-relative overlay positioning + * @param offsetX Horizontal offset from controller in meters + * @param offsetY Vertical offset from controller in meters + * @param offsetZ Depth offset from controller in meters + * @param width Width of the overlay in meters + * @param height Height of the overlay in meters + * @return Transformation matrix for controller-relative positioning + * + * This function creates a transformation matrix that positions an overlay + * relative to a VR controller with the specified dimensions and offsets. + */ + vr::HmdMatrix34_t CreateControllerOverlayTransform(float offsetX, float offsetY, float offsetZ, float width, float height); + /** * @brief Common OpenVR system access pattern with validation * @@ -100,6 +127,17 @@ namespace Util bool HasOverlay() const { return IsValid() && overlay; } }; + /** + * @brief Get UI color for controller device type + * @param device The controller device type + * @param isRecording Whether the device is in recording mode (affects color) + * @return ImVec4 color value for UI rendering + * + * This function provides consistent color coding for different controller + * device types in the UI, with special handling for recording mode. + */ + ImVec4 GetControllerDeviceColor(ControllerDevice device, bool isRecording = false); + /** * @brief Get controller index for our ControllerDevice enum * @param device The controller device type (Primary/Secondary) @@ -150,14 +188,11 @@ namespace Util */ inline Matrix HmdMatrix34ToMatrix(const vr::HmdMatrix34_t& m) { - // OpenVR matrices are row-major but designed for column-vector math (M * v). - // DirectX SimpleMath uses row-vector math (v * M). - // We need to transpose the rotation and move translation to the bottom row. return Matrix( - m.m[0][0], m.m[1][0], m.m[2][0], 0.0f, - m.m[0][1], m.m[1][1], m.m[2][1], 0.0f, - m.m[0][2], m.m[1][2], m.m[2][2], 0.0f, - m.m[0][3], m.m[1][3], m.m[2][3], 1.0f); + m.m[0][0], m.m[0][1], m.m[0][2], m.m[0][3], + m.m[1][0], m.m[1][1], m.m[1][2], m.m[1][3], + m.m[2][0], m.m[2][1], m.m[2][2], m.m[2][3], + 0, 0, 0, 1); } /** @@ -171,19 +206,18 @@ namespace Util inline vr::HmdMatrix34_t MatrixToHmdMatrix34(const Matrix& mat) { vr::HmdMatrix34_t m{}; - // Transpose rotation back (row-vector → column-vector) and extract translation from row 4 m.m[0][0] = mat._11; - m.m[0][1] = mat._21; - m.m[0][2] = mat._31; - m.m[0][3] = mat._41; - m.m[1][0] = mat._12; + m.m[0][1] = mat._12; + m.m[0][2] = mat._13; + m.m[0][3] = mat._14; + m.m[1][0] = mat._21; m.m[1][1] = mat._22; - m.m[1][2] = mat._32; - m.m[1][3] = mat._42; - m.m[2][0] = mat._13; - m.m[2][1] = mat._23; + m.m[1][2] = mat._23; + m.m[1][3] = mat._24; + m.m[2][0] = mat._31; + m.m[2][1] = mat._32; m.m[2][2] = mat._33; - m.m[2][3] = mat._43; + m.m[2][3] = mat._34; return m; } @@ -204,45 +238,24 @@ namespace Util return mat; } + //============================================================================= + // WAND POINTING UTILITIES + //============================================================================= + /** - * @brief Gets the Inter-Pupillary Distance (IPD) from the HMD - * @return IPD in meters, or 0.064 (average human IPD) as fallback + * @brief Computes controller ray intersection with a VR overlay + * @param overlay OpenVR overlay interface + * @param overlayHandle Handle to the overlay to test intersection with + * @param controllerIndex Tracked device index of the controller + * @param outUV Output UV coordinates (0-1 range) of intersection point + * @return true if the controller ray intersects the overlay, false otherwise * - * Tries multiple methods to determine IPD: - * 1. Query Prop_UserIpdMeters_Float property directly - * 2. Calculate from eye-to-head transforms - * 3. Fallback to average human IPD (64mm) + * This function uses OpenVR's ComputeOverlayIntersection to perform ray-casting + * from the controller's position and forward direction to detect if it's pointing + * at the specified overlay. If an intersection is found, the UV coordinates are + * returned in the outUV parameter. */ - inline float GetIPDFromHMD() - { - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - if (!openvr || !openvr->vrSystem) - return 0.064f; // Default fallback IPD in meters - - // Method 1: Query IPD property directly - vr::ETrackedPropertyError error = vr::TrackedProp_UnknownProperty; - float ipd = openvr->vrSystem->GetFloatTrackedDeviceProperty( - vr::k_unTrackedDeviceIndex_Hmd, - vr::Prop_UserIpdMeters_Float, - &error); - - if (error == vr::TrackedProp_Success && ipd > 0.0f && ipd < 0.1f) { - return ipd; - } - - // Method 2: Calculate from eye-to-head transforms - vr::HmdMatrix34_t leftEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Left); - vr::HmdMatrix34_t rightEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Right); - - // Eye separation is in the X translation component (m[0][3]) - float eyeSeparation = std::abs(leftEye.m[0][3] - rightEye.m[0][3]); - - if (eyeSeparation > 0.0f && eyeSeparation < 0.1f) { - return eyeSeparation; - } - - // Fallback to average human IPD - return 0.064f; - } + bool ComputeWandIntersection(vr::IVROverlay* overlay, vr::VROverlayHandle_t overlayHandle, + vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV); } \ No newline at end of file